diff --git a/src/components/solutions/BasicCallout.jsx b/src/components/solutions/BasicCallout.jsx new file mode 100644 index 00000000..870c35f1 --- /dev/null +++ b/src/components/solutions/BasicCallout.jsx @@ -0,0 +1,37 @@ +import classNames from "classnames"; +import { Trans } from "next-i18next"; + +import { AnimatedText } from "@/components/shared/Text"; + +import styles from "./BasicCallout.module.scss"; + +const BasicCallout = ({ titleContent, subtitleKey, className, id }) => { + return ( +
+
+
+ {titleContent && ( + + {titleContent} + + )} + + {subtitleKey && ( + + + + )} +
+
+
+ ); +}; + +export default BasicCallout; diff --git a/src/components/solutions/BasicCallout.module.scss b/src/components/solutions/BasicCallout.module.scss new file mode 100644 index 00000000..08a52929 --- /dev/null +++ b/src/components/solutions/BasicCallout.module.scss @@ -0,0 +1,94 @@ +@import "../../scss/solutions/variables"; + +.BasicCallout { + padding: 64px 24px; + + * { + margin: 0; + padding: 0; + } +} + +.MediaBlock { + position: relative; + aspect-ratio: 7 / 9; + margin-top: 40px; + + img { + object-fit: contain; + } +} + +.Title { + font-size: 40px; + font-weight: 700; + line-height: 1.04; + letter-spacing: -0.01em; + text-align: center; + color: var(--white); +} + +.Subtitle { + font-size: 20px; + font-weight: 700; + line-height: 1.12; + letter-spacing: -0.02em; + text-align: center; + color: var(--grey-250); + margin: 24px auto 0; +} + +@include breakpoint(md) { + .Title { + font-size: 56px; + } + + .WithImage { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + align-items: center; + + h2, + p { + text-align: left; + } + } + + .MediaBlock { + margin-top: 0; + } +} + +@include breakpoint(lg) { + .BasicCallout { + padding: 128px; + } + + .Title { + font-size: 72px; + line-height: 1.08; + letter-spacing: -0.03em; + margin-bottom: 24px; + } + + .Subtitle { + font-size: 24px; + font-weight: 500; + line-height: 1.25; + text-align: center; + max-width: 800px; + } + + .ButtonWrapper { + margin-top: 40px; + } + + .WithImage { + p { + font-size: 32px; + line-height: 1.15; + letter-spacing: -0.64px; + } + } +} diff --git a/src/components/solutions/Button.module.scss b/src/components/solutions/Button.module.scss new file mode 100644 index 00000000..c533c14d --- /dev/null +++ b/src/components/solutions/Button.module.scss @@ -0,0 +1,98 @@ +@import "../../scss/solutions/_variables.scss"; + +.Button { + padding: 12px 24px; + border-radius: 8px; + color: var(--white); + text-align: center; + font-size: 18px; + font-weight: 700; + line-height: 1.32; + position: relative; + cursor: pointer; + display: flex; + justify-content: center; + transition: + transform 0.3s $easeInOutQuart, + box-shadow 0.5s ease-out; + overflow: hidden; + box-shadow: 0 0 0 rgba(#80ecff, 0); + + span { + position: relative; + z-index: 2; + transition: color 0.15s ease-in-out; + } + + /* These are the two backgrounds (before/after hover tied to before/after pseudo elements) */ + &.primary, + &.secondary { + &:before, + &:after { + content: ""; + display: block; + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; + z-index: 1; + border-radius: 8px; + } + } + + // Primary & secondary buttons start with different background colors + &.primary { + &:before { + background: var(--purple); + } + } + + &.secondary { + &:before { + background: var(--grey-450); + } + } + + // Default hide :after and show on hover + &:after { + opacity: 0; + background: var(--purple); + background: var(--gradient-3); + + // animation styling + transition: + opacity 0.3s ease-in-out, + transform 0.2s $easeInOutQuart, + filter 0.35s $easeDefault; + transform: translate(0, 60%) scale(1.2, 1); + filter: blur(15px); + } + + /* On hover, fade in the :after to show the new background. */ + &:hover { + transform: translate(0, -2px); + box-shadow: 0px 2px 12px rgba(#80ecff, 0.5); + &:after { + opacity: 1; + transform: translate(0, 0) scale(1.2, 1); + filter: blur(0); + border-radius: 8px; + } + + span { + color: var(--black); + } + } + + &:active { + transition: transform 0.2s $easeInOutQuart; + transform: translate(0, 0); + box-shadow: 0 1px 6px rgba(#80ecff, 0.35); + + &:after { + transition: opacity 0.15s; + opacity: 0.5; + } + } +} diff --git a/src/components/solutions/Button.tsx b/src/components/solutions/Button.tsx new file mode 100644 index 00000000..2d58d514 --- /dev/null +++ b/src/components/solutions/Button.tsx @@ -0,0 +1,31 @@ +import { AnchorHTMLAttributes } from "react"; +import Link from "next/link"; +import classNames from "classnames"; +import styles from "./Button.module.scss"; + +export interface ButtonProps extends AnchorHTMLAttributes { + text: string; + url?: string; + theme?: "primary" | "secondary"; + classes?: string; +} + +const Button = ({ + text, + url, + theme = "primary", + classes, + ...props +}: ButtonProps) => { + return ( + + {text} + + ); +}; + +export default Button; diff --git a/src/components/solutions/DetailsSection.module.scss b/src/components/solutions/DetailsSection.module.scss new file mode 100644 index 00000000..8517aab4 --- /dev/null +++ b/src/components/solutions/DetailsSection.module.scss @@ -0,0 +1,108 @@ +@import "../../scss/solutions/_variables.scss"; + +.Content { + * { + padding: 0; + margin: 0; + } +} + +.DetailsSection { + display: grid; + grid-template-columns: 1fr; + grid-gap: 40px; + padding: 0 24px 64px; + + a { + svg { + transition: transform 0.25s $easeInOutQuart; + } + + &:hover svg { + transform: translate(4px, 0); + } + } +} + +.Detail { + text-align: center; + + h3, + p { + font-size: 18px; + font-weight: 700; + line-height: 1.16; + color: var(--white); + margin-top: 0; + margin-bottom: 0; + } + + h3 { + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + + svg { + box-sizing: content-box; + width: 22px; + } + } + + p { + color: var(--grey-300); + + a { + color: var(--grey-300); + text-decoration: underline; + } + } +} + +@include breakpoint(md) { + .DetailsSection { + grid-template-columns: 1fr 1fr; + grid-row-gap: 40px; + grid-column-gap: 112px; + padding: 64px 40px; + } + + .Detail { + text-align: left; + + h3, + p { + font-size: 20px; + margin-left: 0; + max-width: 100%; + } + + h3 { + justify-content: flex-start; + + svg { + width: 24px; + } + } + } +} + +@include breakpoint(xl) { + .Content { + padding: 0 0 128px; + } + + .DetailsSection { + grid-row-gap: 80px; + grid-column-gap: 224px; + } + + .Detail { + h3, + p { + font-size: 20px; + line-height: 1.12; + letter-spacing: -0.02em; + } + } +} diff --git a/src/components/solutions/DetailsSection.tsx b/src/components/solutions/DetailsSection.tsx new file mode 100644 index 00000000..3857e735 --- /dev/null +++ b/src/components/solutions/DetailsSection.tsx @@ -0,0 +1,54 @@ +import { ReactNode } from "react"; +import Link from "next/link"; +import classNames from "classnames"; +import styles from "./DetailsSection.module.scss"; +import CaretIcon from "@/components/icons/Caret"; +import { AnimatedText } from "../shared/Text"; + +interface DetailProps { + title: string; + text?: string; + url?: string; + arrow?: boolean; +} + +export const Detail = ({ url, title, text, arrow = false }: DetailProps) => ( +
+ {url ? ( + + + {title} + {arrow && } + + + ) : ( + + {title} + + )} + + {text && ( + + {text} + + )} +
+); + +const DetailsSection = ({ + className, + children, +}: { + className?: string; + children: ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; + +export default DetailsSection; diff --git a/src/components/solutions/DeveloperResources.module.scss b/src/components/solutions/DeveloperResources.module.scss new file mode 100644 index 00000000..9ef69186 --- /dev/null +++ b/src/components/solutions/DeveloperResources.module.scss @@ -0,0 +1,168 @@ +@import "../../scss/solutions/_variables.scss"; + +.DeveloperResources { + padding: 80px 0; + background-color: var(--grey-500); + background-image: url("/solutions/loyalty/gradient-mobile.webp"); + background-size: contain; + background-position: top center; + background-repeat: no-repeat; +} + +.Container { + padding: 0 24px; + margin: 0 auto; + display: flex; + flex-direction: column; + align-items: center; +} + +.TitleBlock { + text-align: center; + + h2 { + font-size: 40px; + font-weight: 400; + line-height: 1.08; + text-transform: uppercase; + font-family: var(--abc-mono-font); + } + + p { + color: var(--grey-250); + font-size: 18px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.18px; + margin: 24px auto 0; + } + + .ButtonWrapper { + a { + max-width: max-content; + margin: 40px auto 64px; + } + } +} + +.LinksWrapper { + margin-top: 40px; + display: flex; + flex-direction: column; + gap: 32px; + list-style-type: none; + padding: 0; + margin: 40px 0 64px; + + li { + text-align: left; + } + + a { + color: var(--grey-100); + font-size: 18px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.01em; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + cursor: pointer; + + svg { + width: 24px; + height: 24px; + transition: 0.2s ease-in; + } + + &:hover { + svg { + transform: translateX(4px); + } + } + } + + span { + color: #6c6a81; + font-size: 13px; + font-weight: 400; + line-height: 1.4; + letter-spacing: 0.26px; + text-transform: uppercase; + display: block; + margin-top: 8px; + } +} + +.ImageWrapper { + position: relative; + display: flex; + justify-content: end; +} + +.CustomMedia { + max-width: 400px; +} + +@include breakpoint(md) { + .DeveloperResources { + background-image: url("/solutions/loyalty/gradient-tablet.webp"); + } + + .TitleBlock { + h2 { + font-size: 48px; + } + } +} + +@include breakpoint(xl) { + .DeveloperResources { + padding: 128px 0; + background-image: url("/solutions/loyalty/gradient-desktop.webp"); + background-position: top left; + } + + .Container { + display: grid; + grid-template-columns: 0.8fr 1fr; + grid-column-gap: 96px; + grid-row-gap: 40px; + align-items: center; + max-width: 1064px; + padding: 0; + } + + .TitleBlock { + text-align: left; + + h2 { + font-size: 56px; + } + + p { + font-size: 24px; + color: var(--grey-300); + margin-left: 0; + max-width: 386px; + } + } + + .LinksWrapper { + margin: 40px 0 0; + width: 100%; + gap: 16px; + + a { + justify-content: flex-start; + } + + span { + font-size: 15px; + line-height: 1.15; + letter-spacing: 0.3px; + margin-top: 10px; + } + } +} diff --git a/src/components/solutions/DeveloperResources.tsx b/src/components/solutions/DeveloperResources.tsx new file mode 100644 index 00000000..dd1f3371 --- /dev/null +++ b/src/components/solutions/DeveloperResources.tsx @@ -0,0 +1,108 @@ +import Link from "next/link"; +import Image from "next/image"; +import { Trans } from "next-i18next"; +import classNames from "classnames"; + +import Button from "@/components/solutions/Button"; +import { AnimatedText } from "@/components/shared/Text"; +import CaretIcon from "@/components/icons/Caret"; + +import styles from "./DeveloperResources.module.scss"; + +import resourcesImage from "../../../assets/solutions/developer-resources.svg"; + +interface DeveloperResourcesLinkProps { + title: string; + text?: string; + link?: string; +} + +export const DeveloperResourcesLink = ({ + title, + text, + link, +}: DeveloperResourcesLinkProps) => ( +
  • + {link && ( + + {title} + + + )} + {text && {text}} +
  • +); + +interface DeveloperResourcesProps { + title: string; + subtitle?: string; + links: React.ReactNode; + id: string; + buttonText?: string; + buttonUrl?: string; + image?: string; + className?: string; + imageClassName?: string; + media?: React.ReactNode; +} + +const DeveloperResources = ({ + title, + subtitle, + links, + id, + buttonText, + buttonUrl, + image, + className, + imageClassName, + media, +}: DeveloperResourcesProps) => { + return ( +
    +
    +
    + + + + + {subtitle && ( + + + + )} + + {links &&
      {links}
    } + + {buttonText && buttonUrl && ( +
    +
    + )} +
    + +
    + {media ? ( +
    {media}
    + ) : ( + {title} + )} +
    +
    +
    + ); +}; + +export default DeveloperResources; diff --git a/src/components/solutions/EcosystemSlider.jsx b/src/components/solutions/EcosystemSlider.jsx new file mode 100644 index 00000000..0d54c625 --- /dev/null +++ b/src/components/solutions/EcosystemSlider.jsx @@ -0,0 +1,89 @@ +import Link from "next/link"; +import Image from "next/image"; +import { Trans } from "next-i18next"; +import classNames from "classnames"; +import { ChevronRight } from "react-feather"; +import Marquee from "react-fast-marquee"; + +import CardsSlider from "@/components/shared/CardsSlider"; +import { AnimatedText, GradientText } from "@/components/shared/Text"; + +import styles from "./EcosystemSlider.module.scss"; + +export const Card = ({ img, url, title, text, className }) => { + const Content = () => ( + <> +
    + {title} +
    +
    + {title} + {url && } +
    +

    {text}

    + + ); + + return ( +
    + {url ? ( + + + + ) : ( + + )} +
    + ); +}; + +const EcosystemSlider = ({ + titleKey, + textKey, + cards, + className, + titleBlockClassName, +}) => { + return ( +
    +
    + {titleKey && ( + + + ), + }} + /> + + )} + + {textKey && ( + + + + )} +
    + +
    + +
    + +
    + +
    + {cards.map((card, index) => ( +
    + {card} +
    + ))} +
    +
    +
    +
    + ); +}; + +export default EcosystemSlider; diff --git a/src/components/solutions/EcosystemSlider.module.scss b/src/components/solutions/EcosystemSlider.module.scss new file mode 100644 index 00000000..847329aa --- /dev/null +++ b/src/components/solutions/EcosystemSlider.module.scss @@ -0,0 +1,236 @@ +@import "../../scss/solutions/_variables.scss"; + +.EcosystemSlider { + padding: 64px 0; + position: relative; +} + +.Title { + font-size: 40px; + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.03em; + text-align: center; + color: var(--white); + text-transform: capitalize; + margin: 0; + padding: 0 24px; + + strong { + background: var(--gradient-2); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + } +} + +.Text { + font-size: 20px; + font-weight: 700; + line-height: 1.12; + letter-spacing: -0.02em; + text-align: center; + color: var(--grey-250); + max-width: 698px; + padding: 0 24px; + margin: 24px auto 0; + + @include breakpoint(md) { + font-size: 24px; + } + + @include breakpoint(xl) { + font-size: 32px; + font-weight: 500; + line-height: 1.25; + text-align: center; + max-width: 886px; + } +} + +.Card { + width: 12rem; + + a { + svg, + h5 { + transition: 0.2s ease-in; + } + + svg { + width: 20px; + } + + img { + transition: transform 0.2s ease-in; + } + + .ImageWrapper { + overflow: hidden; + border-radius: 16px; + } + + &:hover { + svg { + transform: translateX(4px); + } + + img { + transform: scale(1.05); + } + } + } + + h5 { + font-size: 20px; + text-transform: capitalize; + margin-block: 24px 16px !important; + display: flex; + align-items: center; + color: var(--white); + gap: 12px; + } + + p { + font-size: 16px; + color: var(--grey-300); + margin: 0px; + line-height: 1.12; + font-weight: 700; + } + + img { + width: 100%; + height: 100%; + object-fit: cover; + max-height: min(calc(100vw * 0.5), 220px); + border-radius: 16px; + } +} + +.MobileCarousel { + overflow: hidden; + position: relative; + + * { + outline: none !important; + } + + .CarouselArrows { + padding: 32px 0 0; + display: flex; + justify-content: center; + gap: 16px; + width: 100%; + + button { + background: var(--grey-500); + border: none; + border-radius: 50%; + padding: 8px; + cursor: pointer; + transition: background 0.3s; + width: 40px; + height: 40px; + display: flex; + justify-content: center; + align-items: center; + + &.PrevArrow { + transform: rotate(180deg); + } + + &:hover { + background: var(--grey-300); + } + + img { + width: 24px; + height: 24px; + } + + svg { + width: 16px; + width: 16px; + } + } + } +} + +.DesktopMarquee { + position: relative; + margin-top: 80px; + + &:before, + &:after { + content: ""; + width: 300px; + height: 100%; + position: absolute; + left: 0; + top: 0; + z-index: 2; + } + + &:before { + background: linear-gradient( + 90deg, + #0f0a16 0.5%, + rgba(15, 10, 22, 0) 99.21% + ); + } + + &:after { + right: 0 !important; + top: 0; + left: unset; + background: linear-gradient( + 272deg, + #0f0a16 0.5%, + rgba(15, 10, 22, 0) 99.21% + ); + } + + .Cards { + display: flex; + gap: 16px; + margin: 0 8px; + } + + .Card { + max-width: 238px; + padding: 0; + } + + @include breakpoint(xl) { + margin-top: 96px; + } +} + +@include breakpoint(md) { + .Title { + font-size: 56px; + } +} + +@include breakpoint(lg) { + .Title { + font-size: 64px; + } +} + +@include breakpoint(xl) { + .EcosystemSlider { + padding: 128px 0; + } + + .Title { + font-size: 80px; + } + + .Text { + font-size: 28px; + line-height: 1.15; + letter-spacing: -0.64px; + } +} diff --git a/src/components/solutions/EcosystemSliderWithTabs.module.scss b/src/components/solutions/EcosystemSliderWithTabs.module.scss new file mode 100644 index 00000000..2538c8c9 --- /dev/null +++ b/src/components/solutions/EcosystemSliderWithTabs.module.scss @@ -0,0 +1,226 @@ +@import "../../scss/solutions/_variables.scss"; + +.EcosystemSliderWithTabs { + padding: 64px 0; + position: relative; +} + +.Title { + font-size: 40px; + font-weight: 700; + line-height: 1.04; + letter-spacing: -0.01em; + text-align: center; + color: var(--white); + text-transform: capitalize; + margin: 0; + padding: 0 24px; + + strong { + background: var(--gradient-2); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + } +} + +.TabsRoot { + margin-top: 64px; + + @include breakpoint(xl) { + margin-top: 80px; + } +} + +.TabsList { + flex-shrink: 0; + display: flex; + border-radius: 12px; + border: 4px solid var(--grey-450); + background-color: var(--grey-450); + max-width: 343px; + width: 100%; + margin: 0 auto; +} + +.TabsTrigger { + font-family: inherit; + background-color: var(--grey-450); + padding: 0 20px; + height: 45px; + flex: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + line-height: 1; + color: var(--grey-250); + user-select: none; + font-weight: 700; + + &[data-state="active"] { + color: var(--white); + background: var(--grey-300); + border-radius: 4px; + } + + &:focus { + position: relative; + } +} + +.TabsContent { + flex-grow: 1; + padding: 0; + outline: none; + transition: opacity $duration-shorter $easeInQuart; + transition: all 1s; + min-height: 320px; + position: relative; + + &[hidden] { + display: block !important; + opacity: 0; + position: absolute; + top: 0; + } + + &[data-state="inactive"] { + opacity: 0; + } + + &[data-state="active"] { + opacity: 1; + } +} + +.Card { + width: 164px; + + a { + svg { + transition: 0.2s ease-in; + width: 20px; + } + + &:hover { + svg { + transform: translateX(4px); + } + } + } + + h5 { + font-size: 19.42px; + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.02em; + text-align: left; + text-transform: capitalize; + margin-block: 24px 16px !important; + display: flex; + align-items: center; + color: var(--grey-100); + gap: 12px; + } + + p { + color: var(--grey-300); + margin: 0px; + font-size: 14.56px; + font-weight: 700; + line-height: 1.3; + text-align: left; + } + + img { + width: 100%; + height: 100%; + object-fit: cover; + max-height: min(calc(100vw * 0.5), 220px); + border-radius: 16px; + } +} + +.MobileCarousel { + overflow: hidden; + position: relative; +} + +.DesktopMarquee { + position: relative; + margin-top: 80px; + + &:before, + &:after { + content: ""; + width: 300px; + height: 100%; + position: absolute; + left: 0; + top: 0; + z-index: 2; + } + + &:before { + background: linear-gradient( + 90deg, + #0f0a16 0.5%, + rgba(15, 10, 22, 0) 99.21% + ); + } + + &:after { + right: 0 !important; + top: 0; + left: unset; + background: linear-gradient( + 272deg, + #0f0a16 0.5%, + rgba(15, 10, 22, 0) 99.21% + ); + } + + .Cards { + display: flex; + gap: 16px; + margin: 0 8px; + } + + .Card { + width: 12rem; + padding: 0; + } + + @include breakpoint(xl) { + margin-top: 40px; + } +} + +@include breakpoint(md) { + .Title { + font-size: 56px; + } +} + +@include breakpoint(lg) { + .Title { + font-size: 64px; + } +} + +@include breakpoint(xl) { + .EcosystemSliderWithTabs { + padding: 128px 0; + } + + .Title { + font-size: 80px; + } + + .Text { + font-size: 28px; + line-height: 1.15; + letter-spacing: -0.64px; + } +} diff --git a/src/components/solutions/EcosystemSliderWithTabs.tsx b/src/components/solutions/EcosystemSliderWithTabs.tsx new file mode 100644 index 00000000..1626fbe9 --- /dev/null +++ b/src/components/solutions/EcosystemSliderWithTabs.tsx @@ -0,0 +1,155 @@ +import { useState, ReactNode, FC } from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { Trans } from "next-i18next"; +import classNames from "classnames"; +import * as Tabs from "@radix-ui/react-tabs"; +import { ChevronRight } from "react-feather"; +import Marquee from "react-fast-marquee"; + +import CardsSlider from "@/components/shared/CardsSlider"; +import { AnimatedText, GradientText } from "@/components/shared/Text"; + +import styles from "./EcosystemSliderWithTabs.module.scss"; + +export const Card = ({ img, url, title, text }) => { + const Content = () => ( + <> + {title} +
    + {title} + +
    +

    {text}

    + + ); + + return ( +
    + {url ? ( + + + + ) : ( + + )} +
    + ); +}; + +interface EcosystemSliderWithTabsProps { + titleKey: string; + tab1Title: string; + tab2Title: string; + tab1Cards: ReactNode[]; + tab2Cards: ReactNode[]; + className?: string; +} + +const EcosystemSliderWithTabs: FC = ({ + titleKey, + tab1Title, + tab2Title, + tab1Cards, + tab2Cards, + className, +}) => { + const [value, setValue] = useState("tab1"); + + return ( +
    +
    + + + ), + }} + /> + +
    + + + + setValue("tab1")} + > + {tab1Title} + + setValue("tab2")} + > + {tab2Title} + + + + +
    + +
    + +
    + +
    + {tab1Cards.map((card, index) => ( +
    + {card} +
    + ))} +
    +
    +
    +
    + + +
    + +
    + +
    + +
    + {tab2Cards.map((card, index) => ( +
    + {card} +
    + ))} +
    +
    +
    +
    +
    +
    + ); +}; + +export default EcosystemSliderWithTabs; diff --git a/src/components/solutions/FooterCallout.module.scss b/src/components/solutions/FooterCallout.module.scss new file mode 100644 index 00000000..a1a55e03 --- /dev/null +++ b/src/components/solutions/FooterCallout.module.scss @@ -0,0 +1,184 @@ +@import "../../scss/solutions/_variables.scss"; + +.FooterCallout { + border-top: 1px solid var(--grey-450); + border-bottom: 1px solid var(--grey-450); + background: var(--grey-500); + position: relative; +} + +.TopSection, +.ButtonLargeWrapper { + padding-left: 24px; + padding-right: 24px; +} + +.TopSection { + padding-top: 64px; + padding-bottom: 64px; +} + +.Title { + color: var(--grey-100); + text-align: center; + font-size: 32px; + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.03em; + margin: 0; +} + +.TextBtnWrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; +} + +.Text { + color: var(--grey-300); + text-align: center; + font-size: 18px; + font-weight: 700; + line-height: 1.12; + letter-spacing: -0.01em; + margin: 40px auto 0; + max-width: 100%; + + strong { + color: var(--grey-100); + display: block; + margin-bottom: 0; + } +} + +.Button { + font-size: 15px; + font-weight: 700; + line-height: 1.3; + text-align: left; + background: var(--purple); + color: var(--white); + border-radius: 8px; + padding: 12px 30px; + display: block; + width: fit-content; + + &:hover { + background: var(--gradient-3); + color: var(--black); + transition: background 1s ease-out; + } +} + +.ButtonLargeWrapper { + padding: 40px 24px; + border-top: 1px solid var(--grey-450); +} + +.ButtonLarge { + text-align: center; + font-size: 20px; + font-weight: 700; + line-height: 1.2; + padding: 40px 24px; + background-color: var(--purple); + color: var(--white); + border-radius: 8px; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + + svg { + margin-top: 24px; + transition: 0.2s ease-in; + } + + strong { + background: var(--gradient-6); + background-clip: text; + -webkit-text-fill-color: transparent; + text-align: center; + } + + @media (max-width: #{$screen-xl-min - 1}) { + padding: 24px; + display: block; + + svg { + margin: 0; + margin-left: 4px; + } + } + + @include breakpoint(sm) { + text-align: left; + } + + @include breakpoint(xl) { + grid-column: 1 / -1; + margin-top: 0; + flex-direction: row; + justify-content: flex-start; + padding: 40px 50px; + gap: 8px; + font-size: 28px; + + svg { + margin-top: 0; + position: absolute; + right: 48px; + } + + &:before { + display: none; + } + } +} + +@include breakpoint(md) { + .Title { + font-size: 48px; + } +} + +@include breakpoint(xl) { + .TopSection { + display: grid; + grid-template-columns: auto auto; + justify-content: space-between; + padding-top: 80px; + padding-bottom: 80px; + } + + .TextBtnWrapper { + align-items: flex-start; + max-width: 331px; + align-self: flex-end; + } + + .Title { + text-align: left; + font-size: 56px; + max-width: 458px; + } + + .Text { + font-size: 20px; + margin-top: 0; + text-align: left; + } + + .ButtonLargeWrapper { + border: none; + padding: 80px 0; + + svg { + width: 40px; + height: 40px; + } + } +} diff --git a/src/components/solutions/FooterCallout.tsx b/src/components/solutions/FooterCallout.tsx new file mode 100644 index 00000000..b6d1436c --- /dev/null +++ b/src/components/solutions/FooterCallout.tsx @@ -0,0 +1,103 @@ +import Link from "next/link"; +import { Trans } from "next-i18next"; +import classNames from "classnames"; + +import Button from "@/components/solutions/Button"; +import CaretIcon from "@/components/icons/Caret"; +import { AnimatedText } from "@/components/shared/Text"; +import { MotionSlideIn } from "@/components/shared/Motions"; + +import styles from "./FooterCallout.module.scss"; + +interface FooterCalloutProps { + title?: string; + text?: string; + btnText?: string; + btnUrl?: string; + btnLargeText?: string; + btnLargeUrl?: string; + className?: string; + topSectionClassName?: string; + buttonLargeClassName?: string; +} + +const FooterCallout = ({ + title, + text, + btnText, + btnUrl, + btnLargeText, + btnLargeUrl, + className, + topSectionClassName, + buttonLargeClassName, +}: FooterCalloutProps) => { + return ( +
    +
    +
    + {title && ( + + {title} + + )} + +
    + {text && ( + + + + )} + + {btnText && btnUrl && ( + +
    +
    + + {btnLargeText && btnLargeUrl && ( +
    + + {btnLargeUrl.includes("@") ? ( + + + + + ) : ( + + + + + )} + +
    + )} +
    +
    + ); +}; + +export default FooterCallout; diff --git a/src/components/solutions/Layout.module.scss b/src/components/solutions/Layout.module.scss new file mode 100644 index 00000000..2ac6cacc --- /dev/null +++ b/src/components/solutions/Layout.module.scss @@ -0,0 +1,122 @@ +@import "../../scss/solutions/_variables.scss"; + +.Layout { + :global { + // --- Colors --- + --white: #fff; + --grey-100: #eaebf0; + --grey-200: #d0d0dc; + --grey-250: #a2a1b2; + --grey-300: #6c6a81; + --grey-400: #504d61; + --grey-450: #322f43; + --grey-500: #1d1a23; + --black: #0f0a16; + + --pink: #eb54bc; + --purple: #9945ff; + --blue: #64a8f2; + --blue-light: #80ecff; + --green: #14f195; + + // --- Gradient variables --- + --gradient-1: linear-gradient( + 90deg, + #64a8f2 0%, + #9945ff 49.61%, + #eb54bc 100% + ); + --gradient-2: linear-gradient( + 90deg, + #80ecff 0.17%, + #64a8f2 50.54%, + #9945ff 99.77% + ); + --gradient-3: linear-gradient( + 90deg, + #14f195 -6.38%, + #80ecff 51.28%, + #64a8f2 106.12% + ); + --gradient-4: linear-gradient(90deg, #14f195 0%, #64a8f2 50%, #9945ff 100%); + --gradient-5: linear-gradient( + 45deg, + #9945ff 10.43%, + #8752f3 30.84%, + #5497d5 49.4%, + #43b4ca 58.68%, + #28e0b9 69.81%, + #14f195 93.01% + ); + --gradient-6: linear-gradient( + 90deg, + #14f195 -6.38%, + #80ecff 30.93%, + #64a8f2 69.37%, + #64a8f2 106.68% + ); + --gradient-7: linear-gradient(90deg, #9945ff 0%, #64a8f2 50%, #14f195 100%); + --gradient-8: linear-gradient( + 270deg, + #9945ff 0%, + #eb54bc 50.57%, + #ff754a 100% + ); + --gradient-9: linear-gradient( + 90deg, + #ffffff, + #ffffff, + #eb54bc, + #9945ff, + #eb54bc, + #64a8f2, + #ffffff + ); + --gradient-10: linear-gradient( + 90deg, + #64a8f2 0%, + #9945ff 21.95%, + #eb54bc 32.2%, + #ffffff 61.46% + ); + --gradient-11: linear-gradient( + 90deg, + #64a8f2 0%, + #9945ff 41.95%, + #eb54bc 82.2% + ); + + // --- Fonts --- + --abc-mono-font: "ABC Mono", monospace, sans-serif; + + background: var(--black); + + // --- Breakpoint utility classes --- + // Common containers use this max width + .page-width { + @include max-width(1068px); + box-sizing: content-box; + + @include breakpoint(sm) { + padding-left: 40px; + padding-right: 40px; + } + } + } +} + +// --- Overrides --- + +.Header { + &, + nav { + background: var(--black); + } +} + +.Footer { + background: var(--grey-500); + margin-top: 0; + border-radius: 0; + border: none; +} diff --git a/src/components/solutions/LongformItem.module.scss b/src/components/solutions/LongformItem.module.scss new file mode 100644 index 00000000..3669f539 --- /dev/null +++ b/src/components/solutions/LongformItem.module.scss @@ -0,0 +1,362 @@ +@import "../../scss/solutions/_variables.scss"; + +.LongformItem { + padding: 64px 24px; + display: flex; + flex-direction: column; + gap: 40px; + align-items: center; + max-width: 900px; + margin: { + left: auto; + right: auto; + } + + img.desktop-image { + display: none !important; + } + + a { + color: var(--white); + } + + &[data-media-desktop-placement="above"] { + gap: 24px; + + .MediaComponent { + position: relative; + + svg { + max-height: initial; + } + + &:after { + content: ""; + position: absolute; + display: block; + width: 100%; + height: 30%; + background: linear-gradient( + 180deg, + rgba(15, 10, 22, 0) 2.47%, + #0f0a16 79.46% + ); + bottom: 0; + left: 0; + z-index: 1; + } + } + } + + &[data-media-desktop-placement="below"] { + gap: 40px; + flex-direction: column-reverse; + + .MediaComponent { + position: relative; + + svg { + max-height: initial; + } + + &:after { + content: ""; + position: absolute; + display: block; + width: 100%; + height: 30%; + background: linear-gradient( + 180deg, + rgba(15, 10, 22, 0) 2.47%, + #0f0a16 79.46% + ); + bottom: 0; + left: 0; + z-index: 1; + } + } + } +} + +.MediaComponent { + height: auto; + width: min(80vw, 100%); + display: flex; + @include max-width(1020px); + justify-content: center; + + [role="button"] { + cursor: initial; + } + + svg { + max-height: 70vh; + } +} + +@media (max-width: #{$screen-md-min - 1}) { + .MediaComponent { + svg { + max-height: 65vh; + } + } +} + +.Title { + font-size: 32px; + font-weight: 700; + line-height: 1; + letter-spacing: -0.02em; + margin: 0; + width: 100%; + text-align: center; + + a { + color: var(--white); + } +} + +.Subtitle { + font-size: 16px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.01em; + text-align: center; + color: var(--grey-300); + margin: 0; + + a { + color: var(--grey-300) !important; + text-decoration: underline; + } +} + +.TextBlock { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; +} + +.SeeMoreWrapper { + margin-top: 40px; + width: 100%; + + .SeeMoreItemsWrapper { + display: flex; + flex-direction: column; + gap: 24px; + padding-top: 24px; + } + + .LongformSeeMoreItem { + font-size: 16px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.01em; + text-align: center; + color: var(--grey-300); + + strong, + a { + color: var(--grey-250); + } + } +} + +@media (min-width: $screen-md-min) and (max-width: #{$screen-xl-min - 1}) { + .LongformItem { + &[data-media-desktop-placement="left"], + &[data-media-desktop-placement="right"] { + .MediaComponent { + max-width: 300px; + } + } + } +} + +@media (max-width: #{$screen-xl-min - 1}) { + .LongformItem { + max-width: 768px !important; + } +} + +@include breakpoint(md) { + .LongformItem { + padding-top: 64px; + padding-bottom: 64px; + + &[data-media-desktop-placement="above"] { + .SeeMoreWrapper { + .CollapsibleTrigger { + justify-content: center; + } + + .LongformSeeMoreItem { + text-align: center; + } + } + } + + &[data-media-desktop-placement="left"], + &[data-media-desktop-placement="right"] { + > * { + flex: 1; + } + + .MediaComponent { + display: flex; + width: auto; + grid-template-columns: 1fr 1fr; + gap: 40px; + align-items: center; + justify-content: center; + } + + .TextBlock { + max-width: 422px; + gap: 24px; + + * { + text-align: left; + } + } + + .SeeMoreWrapper { + margin-top: 24px; + } + } + + &[data-media-desktop-placement="left"] { + flex-direction: row; + .MediaComponent { + margin-left: 0; + } + } + + &[data-media-desktop-placement="right"] { + flex-direction: row-reverse; + .MediaComponent { + justify-content: flex-end; + } + } + } + + .Title { + font-size: 40px; + font-weight: 700; + line-height: 1; + letter-spacing: -0.02em; + } + + .Subtitle { + @include max-width(600px); + font-size: 20px; + } + + .SeeMoreWrapper { + .CollapsibleTrigger { + justify-content: flex-start; + } + + .CollapsibleTrigger, + .LongformSeeMoreItem { + font-size: 20px; + font-weight: 700; + line-height: 1.12; + letter-spacing: -0.02em; + text-align: left; + } + + .LongformSeeMoreItem { + strong, + a { + color: var(--grey-250); + } + + p { + max-width: 100%; + } + } + } +} + +@include breakpoint(lg) { + .LongformItem { + &[data-media-desktop-placement="left"], + &[data-media-desktop-placement="right"] { + gap: 64px; + } + } +} + +@include breakpoint(xl) { + .LongformItem { + &[data-media-desktop-placement="above"] { + .TextBlock { + &[data-text-content-desktop-direction="column"] { + flex-direction: column; + + .Title { + text-align: center; + } + + .Subtitle { + text-align: center; + } + } + } + + .SeeMoreWrapper { + .CollapsibleTrigger { + justify-content: flex-start; + } + + .LongformSeeMoreItem { + text-align: left; + } + } + } + + &[data-media-desktop-placement="right"] { + .MediaComponent { + margin-right: 0; + } + } + } + + .Title { + font-size: 48px; + text-align: left; + } + + .Subtitle { + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.03em; + text-align: left; + } + + .TextBlock { + &[data-text-content-desktop-direction="row"] { + flex-direction: row; + max-width: initial; + gap: 132px; + align-items: flex-start; + + .Title { + flex: 1; + } + + .SubTextBlock { + flex: 1.2; + } + + .LongformSeeMoreItem { + text-align: left; + } + } + } +} diff --git a/src/components/solutions/LongformItem.tsx b/src/components/solutions/LongformItem.tsx new file mode 100644 index 00000000..159e1536 --- /dev/null +++ b/src/components/solutions/LongformItem.tsx @@ -0,0 +1,124 @@ +/** + * LongformItem component is a versatile layout component designed to display + * media and text content in a structured format. It supports various configurations + * for media placement, text direction, and collapsible content sections. + * + * @component + * @param {ReactNode} [mediaComponent] - The media component to be displayed (e.g., image, video). + * @param {"left" | "right"} [mediaDesktopPlacement] - The placement of the media component on desktop view. If not specified, the media will be placed on top. + * @param {ReactNode} [titleComponent] - The title component to be displayed. + * @param {ReactNode} [subtitleComponent] - The subtitle component to be displayed. + * @param {"row" | "column"} [textContentDesktopDirection] - The direction of the text content on desktop view. Defaults to "column". + * @param {string} [seeMoreTitle] - The title for the collapsible "See More" section. + * @param {ReactNode[]} [seeMoreItems] - The items to be displayed within the "See More" section. + * @param {string} [className] - Additional class names to apply to the component. + * + * @example + * } + * mediaDesktopPlacement="left" + * titleComponent={

    Title

    } + * subtitleComponent={

    Subtitle

    } + * textContentDesktopDirection="column" + * seeMoreTitle="See More" + * seeMoreItems={[

    Item 1

    ,

    Item 2

    ]} + * className="custom-class" + * /> + */ + +import type { ReactNode } from "react"; +import classNames from "classnames"; + +import Text from "@/components/shared/Text"; +import CollapsibleContent from "@/components/shared/CollapsibleContent"; + +import styles from "./LongformItem.module.scss"; + +interface LongformItemProps { + mediaComponent?: ReactNode; + mediaDesktopPlacement?: "left" | "right" | "above"; + titleComponent?: ReactNode; + subtitleComponent?: ReactNode; + textContentDesktopDirection?: "row" | "column"; + seeMoreTitle?: string; + seeMoreItems?: ReactNode[]; + className?: string; + mediaClassName?: string; + customContent?: ReactNode; +} + +export const LongformSeeMoreItem = ({ children }: { children: ReactNode }) => { + return
    {children}
    ; +}; + +const LongformItem = ({ + mediaComponent, + mediaDesktopPlacement = "above", + titleComponent, + subtitleComponent, + textContentDesktopDirection = "column", + seeMoreTitle, + seeMoreItems, + className, + mediaClassName, + customContent, +}: LongformItemProps) => { + return ( +
    + {mediaComponent && ( +
    + {mediaComponent} +
    + )} + +
    + {customContent ? ( +
    {customContent}
    + ) : ( + <> + {titleComponent && ( + + {titleComponent} + + )} + +
    + {subtitleComponent && ( + + {subtitleComponent} + + )} + + {seeMoreTitle && seeMoreItems && ( +
    + +
    + {seeMoreItems.map((item, index) => ( +
    + {item} +
    + ))} +
    +
    +
    + )} +
    + + )} +
    +
    + ); +}; + +export default LongformItem; diff --git a/src/components/solutions/LottieHeroWithTabs.module.scss b/src/components/solutions/LottieHeroWithTabs.module.scss new file mode 100644 index 00000000..633e6afd --- /dev/null +++ b/src/components/solutions/LottieHeroWithTabs.module.scss @@ -0,0 +1,151 @@ +@import "~@/scss/solutions/_variables.scss"; + +.GradientBgWrapper { + position: relative; + width: 100%; + height: 100%; +} + +.GradientBg { + width: 100% !important; + height: auto !important; + z-index: 0; + position: absolute; +} + +.TabsRoot { + display: flex; + flex-direction: column; + padding: 24px 0; + margin-top: 40px; + position: relative; + + @include breakpoint(lg) { + padding-bottom: 9rem; + } +} + +.TabsList { + flex-shrink: 0; + display: flex; + border-radius: 12px; + border: 4px solid var(--grey-500); + background-color: var(--grey-500); + max-width: 343px; + width: 100%; + margin: 20px auto; +} + +.TabsTrigger { + font-family: inherit; + background-color: var(--grey-500); + padding: 0 16px; + height: 45px; + flex: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + line-height: 1; + color: var(--grey-250); + user-select: none; + font-weight: 700; + + &[data-state="active"] { + color: var(--grey-100); + background: var(--grey-300); + border-radius: 8px; + } + + &:focus { + position: relative; + } +} + +.TabsContent { + flex-grow: 1; + padding: 0; + outline: none; + transition: + opacity $duration-shorter $easeInQuart, + transform $duration-shorter $easeInQuart; + + &[hidden] { + display: block !important; + opacity: 0; + transform: translateY($anim-translate-y); + } + + &[data-state="inactive"] { + opacity: 0; + } + + &[data-state="active"] { + opacity: 1; + transform: translateY(0); + } +} + +.Title { + font-size: 44px; + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.02em; + text-align: center; + color: var(--white); + @include max-width(950px); + padding: 0 24px; +} + +.Subtitle { + font-size: 24px; + font-weight: 700; + line-height: 1.12; + letter-spacing: -0.02em; + text-align: center; + margin-top: 20px; + color: var(--grey-250); + max-width: 771px; + margin: 20px auto 0; + padding: 0 24px; +} + +.LottieWrapper { + width: 100%; + max-width: 300px; + margin: 40px auto 0; +} + +@include breakpoint(md) { + .Title { + font-size: 64px; + } +} + +@include breakpoint(lg) { + .Title { + font-size: 80px; + padding: 0 72px; + } + + .LottieWrapper { + max-width: 1000px; + margin-top: 100px; + padding: 0 24px; + } +} + +@include breakpoint(xl) { + .TabsRoot { + margin-top: 4rem; + } + + .Title { + line-height: 1; + } + + .Subtitle { + font-size: 28px; + line-height: 1.25; + } +} diff --git a/src/components/solutions/LottieHeroWithTabs.tsx b/src/components/solutions/LottieHeroWithTabs.tsx new file mode 100644 index 00000000..35179224 --- /dev/null +++ b/src/components/solutions/LottieHeroWithTabs.tsx @@ -0,0 +1,191 @@ +import { useState, ReactNode } from "react"; +import Image from "next/image"; +import classNames from "classnames"; +import * as Tabs from "@radix-ui/react-tabs"; +import Lottie from "react-lottie"; + +import { OpacityInText } from "@/components/shared/Text"; +import { MotionSlideIn } from "@/components/shared/Motions"; + +import styles from "@/components/solutions/LottieHeroWithTabs.module.scss"; + +interface LottieHeroWithTabsProps { + tabs: { + buttonTitle: string; + content: { + title: string; + subtitle: string; + lottieMobile: ReactNode; + lottieDesktop: ReactNode; + }; + }; + tabListAriaLabel: string; + className?: string; +} + +const LottieHeroWithTabs = ({ + tabs, + tabListAriaLabel, + className, +}: LottieHeroWithTabsProps) => { + const [value, setValue] = useState("tab1"); + + return ( + <> + + +
    + Gradient background + Gradient background +
    +
    + + +
    + Gradient background + Gradient background +
    +
    + + + + {tabs[0].content.title} + + + + {tabs[0].content.subtitle} + + + + + + {tabs[1].content.title} + + + {tabs[1].content.subtitle} + + + + + + setValue("tab1")} + > + {tabs[0].buttonTitle} + + setValue("tab2")} + > + {tabs[1].buttonTitle} + + + + + + +
    + +
    +
    + +
    +
    +
    + + + +
    + +
    +
    + +
    +
    +
    +
    + + ); +}; + +export default LottieHeroWithTabs; diff --git a/src/components/solutions/MediaOptionSelection.module.scss b/src/components/solutions/MediaOptionSelection.module.scss new file mode 100644 index 00000000..7dc09644 --- /dev/null +++ b/src/components/solutions/MediaOptionSelection.module.scss @@ -0,0 +1,132 @@ +@import "~@/scss/solutions/_variables.scss"; + +.MediaOptionSelection { + display: flex; + flex-direction: column; + padding: 64px 24px; + gap: 40px; + + @include breakpoint(md) { + flex-direction: row; + } + + @include breakpoint(lg) { + padding: { + top: 84px; + bottom: 84px; + } + } +} + +.MediaWrapper { + @include breakpoint(md) { + flex: 1; + + svg { + height: 100%; + width: auto; + } + + div { + display: flex; + justify-content: center; + } + } +} + +.ContentWrapper { + @include breakpoint(md) { + width: 300px; + display: flex; + flex-direction: column; + justify-content: center; + } + + @include breakpoint(lg) { + width: 423px; + } + + h3 { + color: var(--grey-250); + font-weight: 700; + font-size: 24px; + line-height: 1.12; + text-align: center; + letter-spacing: -0.02em; + margin: 0; + margin-bottom: 16px; + + @include breakpoint(md) { + text-align: left; + line-height: 1.15; + letter-spacing: -0.03em; + margin-bottom: 28px; + } + + @include breakpoint(lg) { + font-size: 28px; + } + } + + ul { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 24px; + padding: 0; + + @include breakpoint(md) { + gap: 0; + } + + @include breakpoint(lg) { + margin-bottom: 40px; + } + } + + li { + font-weight: 700; + font-size: 18px; + line-height: 1.16; + text-align: center; + letter-spacing: -0.01em; + margin: 0; + list-style: none; + + @include breakpoint(md) { + text-align: left; + line-height: 1.12; + letter-spacing: -0.02em; + padding: 18px 16px; + + &.Active { + background: var(--grey-500); + border-radius: 8px; + } + } + + @include breakpoint(lg) { + font-size: 24px; + } + } + + a { + width: max-content; + margin: 0 auto; + + @media (max-width: #{$screen-lg-min - 1}) { + font-size: 15px; + } + + @include breakpoint(md) { + margin-left: 0; + } + + @include breakpoint(lg) { + padding: { + left: 45px; + right: 45px; + } + } + } +} diff --git a/src/components/solutions/MediaOptionSelection.tsx b/src/components/solutions/MediaOptionSelection.tsx new file mode 100644 index 00000000..fb09d146 --- /dev/null +++ b/src/components/solutions/MediaOptionSelection.tsx @@ -0,0 +1,69 @@ +import { useState, FC, ReactNode, useRef } from "react"; +import clsx from "clsx"; +import Button from "@/components/solutions/Button"; +import styles from "./MediaOptionSelection.module.scss"; +import { AnimatedText } from "../shared/Text"; + +interface MediaOption { + label: string; + media: ReactNode; +} + +interface MediaOptionSelectionProps { + options: MediaOption[]; + title?: string; + buttonText?: string; + buttonUrl?: string; +} + +const MediaOptionSelection: FC = ({ + options, + title, + buttonText, + buttonUrl, +}: MediaOptionSelectionProps) => { + const [selectedIndex, setSelectedIndex] = useState(0); + const container = useRef(null); + + const handleSelect = (index: number) => { + setSelectedIndex(index); + + if (container.current) { + container.current.scrollIntoView({ behavior: "smooth" }); + } + }; + + return ( +
    +
    {options[selectedIndex].media}
    + +
    + {title && {title}} + +
      + {options.map((option, index) => ( +
    • + +
    • + ))} +
    + + {buttonText && buttonUrl && ( +
    +
    + ); +}; + +export default MediaOptionSelection; diff --git a/src/components/solutions/Stats.jsx b/src/components/solutions/Stats.jsx new file mode 100644 index 00000000..666244c2 --- /dev/null +++ b/src/components/solutions/Stats.jsx @@ -0,0 +1,121 @@ +import { useRef } from "react"; +import { Trans } from "next-i18next"; +import classNames from "classnames"; +import gsap from "gsap"; +import { useGSAP } from "@gsap/react"; +import { ScrollTrigger } from "gsap/dist/ScrollTrigger"; +import Link from "next/link"; +import { MotionSlideIn } from "@/components/shared/Motions"; +import { AnimatedText } from "@/components/shared/Text"; + +import styles from "./Stats.module.scss"; + +gsap.registerPlugin(useGSAP, ScrollTrigger); + +const Stats = ({ + titleContent, + subtitleKey, + kickerKey, + kickerUrl, + buttonsComponent, + stats, + className, + buttonsClassName, + statsClassName, +}) => { + const container = useRef(null); + + useGSAP( + () => { + const titles = gsap.utils.toArray(".stats-title"); + + titles.forEach((title) => { + gsap.from(title, { + backgroundPosition: "100% 0%", + ease: "none", + scrollTrigger: { + trigger: title, + start: "top 80%", + end: "top 10%", + scrub: 1, + }, + }); + }); + }, + { scope: container }, + ); + + return ( +
    +
    +
    + {titleContent && ( + + {titleContent} + + )} + + {subtitleKey && ( + + + + )} + + {buttonsComponent && ( + + {buttonsComponent} + + )} +
    + +
    + {kickerKey && !kickerUrl && ( + + {kickerKey} + + )} + + {kickerKey && kickerUrl && ( + + + {kickerKey} + + + )} + + {stats.map((stat, index) => ( +
    +

    + {stat.value} +

    +

    {stat.label}

    +
    + ))} +
    +
    +
    + ); +}; + +export default Stats; diff --git a/src/components/solutions/Stats.module.scss b/src/components/solutions/Stats.module.scss new file mode 100644 index 00000000..6bbe86eb --- /dev/null +++ b/src/components/solutions/Stats.module.scss @@ -0,0 +1,174 @@ +@import "@/scss/solutions/_variables.scss"; + +.Stats { + padding: 64px 24px; + position: relative; +} + +.Title { + font-size: 32px; + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.03em; + text-align: center; + color: var(--white); + margin-bottom: 0; +} + +.StatsKicker { + color: #6c6a81; + font-size: 13px; + font-style: normal; + font-weight: 700; + line-height: 1.32; + text-align: center; + display: block; + max-width: 100%; + width: 100%; + margin: 0; + + @include breakpoint(xl) { + font-weight: 700; + line-height: 1.32; + text-align: left; + } +} + +.Subtitle { + font-size: 20px; + font-weight: 700; + line-height: 1.12; + letter-spacing: -0.02em; + text-align: center; + margin: 24px auto 0; + color: var(--grey-250); +} + +.ButtonContainer { + display: flex; + flex-direction: column; + gap: 16px; + margin: 40px auto 0; + width: max-content; + + a { + width: 100%; + } +} + +.StatsContainer { + display: flex; + flex-direction: column; + gap: 40px; + margin-top: 64px; +} + +.Stat { + text-align: center; + display: flex; + flex-direction: column; + gap: 8px; +} + +.StatValue { + font-size: 48px; + font-weight: 700; + line-height: 1; + letter-spacing: -0.02em; + margin: 0; + + background-position: 0% 0%; + --bg-size: 700%; + background: var(--gradient-9) 0 0 / var(--bg-size) 100%; + color: transparent; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + + &::selection { + -webkit-text-fill-color: var(--black); + } +} + +.StatLabel { + font-size: 24px; + font-weight: 500; + line-height: 1.3; + letter-spacing: -0.02em; + color: var(--grey-250); + margin: 0; +} + +@include breakpoint(md) { + .Stats { + padding: 90px 40px; + } + + .Title { + font-size: 40px; + } +} + +@include breakpoint(lg) { + .Title { + font-size: 56px; + } + + .StatValue { + font-size: 64px; + } +} + +@include breakpoint(xl) { + .Stats { + padding: 128px; + } + + .Container { + display: flex; + gap: 66px; + } + + .ContentContainer { + flex: 1; + } + + .Title { + font-size: 56px; + line-height: 1; + text-align: left; + max-width: 473px; + } + + .Subtitle { + font-size: 24px; + line-height: 1.15; + text-align: left; + max-width: 400px; + margin-left: 0; + } + + .ButtonContainer { + margin-left: 0; + } + + .StatsContainer { + margin-top: 0; + width: min-content; + min-width: 327px; + gap: 48px; + } + + .Stat { + text-align: left; + gap: 4px; + } + + .StatValue { + font-size: 72px; + } + + .StatLabel { + font-size: 24px; + } +} diff --git a/src/components/solutions/SuccessStories.module.scss b/src/components/solutions/SuccessStories.module.scss new file mode 100644 index 00000000..a92b6efb --- /dev/null +++ b/src/components/solutions/SuccessStories.module.scss @@ -0,0 +1,194 @@ +@import "../../scss/solutions/_variables.scss"; + +.SuccessStories { + padding: 64px 0; + background: var(--grey-500); + + &[data-theme="black"] { + background: var(--black); + } +} + +.Container { + padding: 0 24px; +} + +.Title { + color: var(--grey-100); + text-align: center; + font-size: 36px; + font-weight: 700; + line-height: 1.15; + letter-spacing: -1.2px; + margin-bottom: 0; +} + +.CardsWrapper { + display: grid; + grid-gap: 40px; + margin-top: 64px; +} + +.StoryCard { + background: var(--grey-450); + padding: 32px 20px; + display: flex; + flex-direction: column; + gap: 20px; + align-items: flex-start; + border-radius: 8px; + height: 100%; + + strong { + color: var(--white); + } + + a { + font-size: 15px; + line-height: 1.32; + } +} + +.LogoWrapper { + margin: 0 auto; +} + +.MainImageWrapper { + width: 100%; + + img { + border-radius: 8px; + width: 100%; + max-height: 400px; + object-fit: cover; + } +} + +.Text { + margin: 0; + color: var(--grey-250); + font-size: 16px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.18px; +} + +@include breakpoint(sm) { + .Text { + max-width: 100%; + } +} + +@include breakpoint(md) { + .Container { + padding: 0 40px; + } + + .Title { + font-size: 48px; + } + + .CardsWrapper { + &:not(.SingleCard) { + grid-template-columns: 1fr 1fr; + + &[data-cards-count="2"] { + max-width: 50rem; + margin: { + left: auto; + right: auto; + } + } + + .Text { + flex: 1; + } + } + } + + .StoryCard a { + font-size: 18px; + line-height: 1.1; + } + + .Text { + font-size: 18px; + line-height: 1.12; + letter-spacing: -0.02em; + flex: 1; + } +} + +@include breakpoint(lg) { + .SuccessStories { + padding: 128px 0 144px; + } + + .Container { + padding: 0; + } + + .CardsWrapper { + margin-top: 80px; + } + + .StoryCard { + padding: 40px; + } +} + +@include breakpoint(xl) { + .Title { + font-size: 64px; + line-height: 1.04; + letter-spacing: -1.6px; + text-transform: capitalize; + } +} + +.SingleCard { + @include breakpoint(md) { + .StoryCard { + display: grid; + grid-template-columns: 0.84fr 1fr; + grid-template-rows: 1fr auto 1fr; + gap: 24px; + } + + .MainImageWrapper { + grid-row: 1 / span 3; + grid-column: 1; + align-self: center; + } + + .LogoWrapper { + grid-column: 2; + grid-row: 1; + margin-left: 0; + align-self: flex-end; + } + + .Text { + grid-column: 2; + grid-row: 2; + } + + a { + grid-column: 2; + grid-row: 3; + width: fit-content; + } + } + + @include breakpoint(lg) { + .StoryCard { + grid-column-gap: 40px; + } + } + + @include breakpoint(xl) { + .StoryCard { + grid-row-gap: 40px; + } + } +} diff --git a/src/components/solutions/SuccessStories.tsx b/src/components/solutions/SuccessStories.tsx new file mode 100644 index 00000000..066cffa7 --- /dev/null +++ b/src/components/solutions/SuccessStories.tsx @@ -0,0 +1,128 @@ +import { ReactNode } from "react"; +import Image from "next/image"; +import classNames from "classnames"; + +import { AnimatedText } from "@/components/shared/Text"; +import { MotionSlideIn } from "@/components/shared/Motions"; +import Button from "./Button"; + +import styles from "./SuccessStories.module.scss"; + +interface StoryCardProps { + logo: string; + logoAlt: string; + mobileImage: string; + desktopImage: string; + imageAlt: string; + text: string; + buttonText: string; + buttonUrl: string; + className?: string; + logoClassName?: string; + mainImageClassName?: string; +} + +export const StoryCard = ({ + logo, + logoAlt, + mobileImage, + desktopImage, + imageAlt, + text, + buttonText, + buttonUrl, + className, + logoClassName, + mainImageClassName, +}: StoryCardProps) => { + return ( +
    +
    + + {logoAlt} + +
    + +
    + + {mobileImage && ( + {imageAlt} + )} + + {imageAlt} + +
    + + + {text} + + + +
    + ); +}; + +interface SuccessStoriesProps { + title: string; + cards: ReactNode[]; + backgroundTheme?: "grey" | "black"; + className?: string; + cardsClassName?: string; + id?: string; +} + +const SuccessStories = ({ + title, + cards, + backgroundTheme = "grey", + className, + cardsClassName, + id, +}: SuccessStoriesProps) => { + return ( +
    +
    + + {title} + + +
    + {cards} +
    +
    +
    + ); +}; + +export default SuccessStories; diff --git a/src/components/solutions/VideoBgHero.module.scss b/src/components/solutions/VideoBgHero.module.scss new file mode 100644 index 00000000..60629926 --- /dev/null +++ b/src/components/solutions/VideoBgHero.module.scss @@ -0,0 +1,188 @@ +@import "~@/scss/solutions/_variables.scss"; + +.ContentWrapper { + padding: 64px 24px; + display: flex; + flex-direction: column; + gap: 24px; + align-items: center; + + h1 { + font-size: 36px; + font-weight: 700; + line-height: 1.14; + letter-spacing: -0.01em; + text-align: center; + margin-bottom: 0; + } + + .Subtitle { + font-size: 20px; + font-weight: 700; + line-height: 1.12; + text-align: center; + color: var(--grey-250); + margin-bottom: 0; + margin-top: 24px; + max-width: 100%; + } + + .ButtonWrapper { + display: flex; + flex-direction: column; + align-items: center; + width: max-content; + margin: 24px auto 0; + gap: 16px; + min-width: 211px; + + @include breakpoint(lg) { + min-width: initial; + margin-left: 0; + align-items: flex-start; + } + } + + a { + width: 100%; + margin: 0; + + @media (max-width: #{$screen-lg-min - 1}) { + font-size: 15px; + font-weight: 700; + line-height: 1.32; + text-align: left; + } + + @include breakpoint(lg) { + width: auto; + } + } +} + +.Eyebrow { + color: #64a8f2; + font-size: 16px; + font-style: normal; + font-weight: 800; + line-height: 1.25; + letter-spacing: -0.32px; + text-align: center; + margin-bottom: 24px; + + @include breakpoint(lg) { + font-size: 22px; + font-weight: 800; + line-height: 1.25; + letter-spacing: -0.44px; + text-align: left; + } +} + +.VideoWrapper { + position: relative; + + &:after { + content: ""; + display: block; + width: 100%; + height: 50%; + position: absolute; + top: 0; + left: 0; + background: linear-gradient( + 180deg, + #0f0a16 1.64%, + rgba(15, 10, 22, 0) 100% + ); + z-index: 1; + } + + &, + video { + width: 100%; + } +} + +@include breakpoint(sm) { + .ContentWrapper { + padding-top: 90px; + padding-bottom: 90px; + + h1 { + font-size: 64px; + } + + .Subtitle { + font-size: 24px; + } + } +} + +@include breakpoint(lg) { + .VideoBgHero { + display: flex; + min-height: calc(100vh - 76px); + position: relative; + } + + .ContentWrapper { + position: relative; + z-index: 1; + width: 100%; + justify-content: center; + align-items: flex-start; + + .ContentInnerWrapper { + max-width: 519px; + } + + h1, + .Subtitle { + text-align: left; + } + + h1 { + font-size: 72px; + font-weight: 700; + line-height: 1.04; + letter-spacing: -0.02em; + text-align: left; + } + + .Subtitle { + font-size: 28px; + font-weight: 700; + line-height: 1.15; + margin-top: 24px; + letter-spacing: -0.02em; + text-align: left; + } + } + + .VideoWrapper { + position: absolute; + z-index: 0; + height: 100%; + + &:after { + content: ""; + display: block; + position: absolute; + height: 100%; + width: 70%; + background: linear-gradient( + 270deg, + rgba(15, 10, 22, 0) 3.27%, + rgba(15, 10, 22, 0.84) 30.48% + ); + top: 0; + left: 0; + } + + video { + height: 100%; + object-fit: cover; + } + } +} diff --git a/src/components/solutions/VideoBgHero.tsx b/src/components/solutions/VideoBgHero.tsx new file mode 100644 index 00000000..13a9a839 --- /dev/null +++ b/src/components/solutions/VideoBgHero.tsx @@ -0,0 +1,115 @@ +/** + * VideoBgHero component renders a section with a video background and content overlay. + * + * @param {string} videoSrc - The source URL for the video to be displayed. + * @param {string} [videoSrc720] - The source URL for the video to be displayed on screens with a max-width of 720px. + * @param {string} videoPoster - The URL of the image to be shown while the video is loading or if the video cannot be played. + * @param {string} title - The main title text to be displayed. + * @param {string} [subtitle] - The subtitle text to be displayed. + * @param {string} [eyebrow] - The eyebrow text to be displayed above the title. + * @param {ButtonProps[]} [buttons] - An array of button properties to render buttons within the component. + * @param {string} [classes] - Additional CSS classes to apply to the component. + * + * @returns {JSX.Element} The rendered VideoBgHero component. + */ +import { useEffect, useRef } from "react"; +import classNames from "classnames"; + +import useReducedMotion from "@/hooks/useReducedMotion"; +import Button, { ButtonProps } from "@/components/solutions/Button"; +import { OpacityInText } from "@/components/shared/Text"; +import { MotionSlideIn } from "@/components/shared/Motions"; + +import styles from "./VideoBgHero.module.scss"; + +interface VideoBgHeroProps { + videoSrc: string; + videoSrc720?: string; + videoPoster: string; + title: string; + subtitle?: string; + eyebrow?: string; + buttons?: ButtonProps[]; + classes?: string; +} + +const VideoBgHero = ({ + videoSrc, + videoSrc720, + videoPoster, + title, + subtitle, + eyebrow, + buttons, + classes, +}: VideoBgHeroProps) => { + const [prefersReducedMotion] = useReducedMotion(); + const videoRef = useRef(null); + + useEffect(() => { + if (videoRef.current && prefersReducedMotion) { + videoRef.current.pause(); + } + }, [prefersReducedMotion]); + + return ( +
    +
    +
    + {eyebrow && ( + + {eyebrow} + + )} + + + {title} + + + {subtitle && ( + + {subtitle} + + )} + + {buttons && ( + +
    + {buttons?.map((button, index) => ( +
    +
    + )} +
    +
    + +
    + +
    +
    + ); +}; + +export default VideoBgHero; diff --git a/src/components/solutions/blinks-and-actions/BlinksHero.jsx b/src/components/solutions/blinks-and-actions/BlinksHero.jsx new file mode 100644 index 00000000..2e40bbbb --- /dev/null +++ b/src/components/solutions/blinks-and-actions/BlinksHero.jsx @@ -0,0 +1,83 @@ +import { useEffect, useRef } from "react"; +import { useTranslation } from "next-i18next"; +import classNames from "classnames"; +import Image from "next/image"; + +import useReducedMotion from "@/hooks/useReducedMotion"; + +import Button from "@/components/solutions/Button"; +import { OpacityInText } from "@/components/shared/Text"; +import { MotionSlideIn } from "@/components/shared/Motions"; + +import styles from "./BlinksHero.module.scss"; + +const BlinksHero = () => { + const { t } = useTranslation(); + + const videoRef = useRef(null); + const [prefersReducedMotion] = useReducedMotion(); + + useEffect(() => { + prefersReducedMotion && videoRef.current.pause(); + }, [prefersReducedMotion]); + + return ( +
    +
    + + {t("solutions-blinks-and-actions.hero.kicker")} + + + + {t("solutions-blinks-and-actions.hero.title")} + + + + {t("solutions-blinks-and-actions.hero.subtitle")} + + + +
    + + + + +
    + ); +}; + +export default BlinksHero; diff --git a/src/components/solutions/blinks-and-actions/BlinksHero.module.scss b/src/components/solutions/blinks-and-actions/BlinksHero.module.scss new file mode 100644 index 00000000..c8b9c057 --- /dev/null +++ b/src/components/solutions/blinks-and-actions/BlinksHero.module.scss @@ -0,0 +1,124 @@ +@import "../../../scss/solutions/_variables.scss"; + +.BlinksHero { + padding: 64px 24px; + display: flex; + flex-direction: column; + gap: 40px; + + .ContentWrapper { + display: flex; + flex-direction: column; + gap: 24px; + + p, + h1 { + margin-top: 0; + margin-bottom: 0; + } + } + + .Kicker { + font-size: 16px; + font-weight: 800; + line-height: 1.25; + letter-spacing: -0.02em; + text-align: center; + color: var(--blue); + } + + .Title { + font-size: 36px; + font-weight: 700; + line-height: 1.14; + letter-spacing: -0.01em; + text-align: center; + } + + .Subtitle { + font-size: 20px; + font-weight: 700; + line-height: 1.2; + text-align: center; + color: var(--grey-250); + } + + .Buttons { + display: flex; + flex-direction: column; + gap: 16px; + width: max-content; + margin: 0 auto; + } + + .ImageWrapper { + display: flex; + justify-content: center; + } +} + +@include breakpoint(md) { + .BlinksHero { + padding: 80px 24px; + + .ContentWrapper { + @include max-width(650px); + } + + .Title { + font-size: 64px; + } + + .Subtitle { + font-size: 24px; + } + } +} + +@include breakpoint(xl) { + .BlinksHero { + padding: 92px 24px; + flex-direction: row; + justify-content: space-between; + gap: 128px; + + .ContentWrapper { + max-width: 520px; + justify-content: center; + flex: 1; + + * { + text-align: left; + } + } + + .Kicker { + margin-left: 0; + font-size: 22px; + } + + .Title { + font-size: 64px; + line-height: 1; + letter-spacing: -0.02em; + max-width: 422px; + } + + .Subtitle { + font-size: 32px; + line-height: 1.15; + letter-spacing: -0.02em; + max-width: 100%; + } + + .Buttons { + margin-left: 0; + min-width: 328px; + + a { + padding: 12px; + width: 100%; + } + } + } +} diff --git a/src/components/solutions/gaming/GamesKit.jsx b/src/components/solutions/gaming/GamesKit.jsx new file mode 100644 index 00000000..04ae56d6 --- /dev/null +++ b/src/components/solutions/gaming/GamesKit.jsx @@ -0,0 +1,89 @@ +import Link from "next/link"; +import classNames from "classnames"; +import { useTranslation, Trans } from "next-i18next"; + +import Button from "@/components/solutions/Button"; +import CaretIcon from "@/components/icons/Caret"; +import { AnimatedText, GradientText } from "@/components/shared/Text"; +import { MotionSlideIn } from "@/components/shared/Motions"; + +import styles from "./GamesKit.module.scss"; + +const GamesKit = () => { + const { t } = useTranslation(); + + const ListItem = ({ title, text, url }) => ( +
    +
    + + {title} + + +
    +

    {text}

    +
    + ); + + return ( +
    +
    +
    + + + + Start with the
    + + Solana Games Kit + +
    +
    +
    + + + {t("solutions-gaming.games-kit.subtitle")} + + + +
    + +
    + + + + + + + + + + + + + + + +
    +
    +
    + ); +}; + +export default GamesKit; diff --git a/src/components/solutions/gaming/GamesKit.module.scss b/src/components/solutions/gaming/GamesKit.module.scss new file mode 100644 index 00000000..605182d1 --- /dev/null +++ b/src/components/solutions/gaming/GamesKit.module.scss @@ -0,0 +1,172 @@ +@import "~@/scss/solutions/_variables.scss"; + +.GamesKit { + background: var(--grey-500); + border-top: 1px solid var(--grey-450); + + @include breakpoint(sm) { + padding: 0 40px; + } +} + +.Container { + @include breakpoint(xl) { + display: flex; + padding: 128px 40px; + gap: 225px; + + .TextBlock, + .ListItems { + padding: 0; + } + } +} + +.TextBlock { + padding: 64px 24px; + + h2 { + font-size: 32px; + font-weight: 700; + line-height: 1.11; + letter-spacing: -0.01em; + text-align: center; + margin: 0; + + strong { + @include gradient-text( + linear-gradient(90deg, #64a8f2 0%, #9945ff 49.61%, #eb54bc 100%) + ); + } + } + + p { + font-size: 18px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.01em; + text-align: center; + color: var(--grey-250); + margin: 24px 0 40px; + max-width: 100%; + } + + a { + width: max-content; + font-size: 15px; + font-weight: 700; + line-height: 1.32; + margin: 0 auto; + } +} + +.ListItems { + padding: 0 24px 64px; + display: flex; + flex-direction: column; + gap: 32px; + + .ListItem { + h5 { + font-size: 18px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.01em; + text-align: center; + margin: 0; + + a { + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + color: var(--grey-100); + + svg { + width: 22px; + transition: 0.2s ease-in; + + @include breakpoint(xl) { + width: 24px; + } + } + + &:hover { + svg { + transform: translateX(4px); + } + } + } + } + + p { + font-size: 18px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.01em; + text-align: center; + color: var(--grey-300); + margin: 8px 0 0; + } + } +} + +@include breakpoint(sm) { + .TextBlock { + h2 { + font-size: 40px; + } + } +} + +@include breakpoint(md) { + .TextBlock { + h2 { + font-size: 56px; + } + } +} + +@include breakpoint(xl) { + .TextBlock { + h2 { + font-weight: 700; + line-height: 1; + letter-spacing: -0.02em; + text-align: left; + } + + p { + font-size: 20px; + font-weight: 700; + line-height: 1.12; + letter-spacing: -0.02em; + text-align: left; + } + + a { + font-size: 18px; + font-weight: 700; + line-height: 1.1; + margin-left: 0; + } + } + + .ListItems { + .ListItem { + h5 a, + p { + text-align: left; + justify-content: flex-start; + } + + h5, + p { + font-size: 20px; + font-weight: 700; + line-height: 1.12; + letter-spacing: -0.02em; + } + } + } +} diff --git a/src/components/solutions/gaming/GamingSlider.module.scss b/src/components/solutions/gaming/GamingSlider.module.scss new file mode 100644 index 00000000..00cd1167 --- /dev/null +++ b/src/components/solutions/gaming/GamingSlider.module.scss @@ -0,0 +1,186 @@ +@import "~@/scss/solutions/_variables.scss"; + +.Container { + position: relative; + + h5, + p { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + h5 { + font-size: 18px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.01em; + text-align: left; + color: var(--grey-100); + margin: 24px 0 0; + display: flex; + gap: 4px; + align-items: center; + + svg { + width: 22px; + + @include breakpoint(md) { + width: 24px; + } + } + + &:hover { + svg { + transform: translateX(4px); + } + } + } + + p { + color: #6c6a81; + font-size: 14px; + line-height: 1.3; + + @include breakpoint(md) { + font-size: 18px; + } + } + + a { + color: var(--white); + } + + @include breakpoint(lg) { + padding: 0; + } +} + +.Slider { + max-width: 1024px; + margin: 0 auto; + position: relative; + padding-bottom: 80px; + + @include breakpoint(lg) { + padding: 0; + + & > div { + padding-top: 48px !important; + padding-bottom: 168px !important; + } + + &:before { + content: ""; + width: 200px; + height: 100%; + position: absolute; + left: -2px; + top: 0; + background: linear-gradient( + 90deg, + #0d0817 20%, + rgba(15, 10, 22, 0) 99.21% + ); + z-index: 10; + pointer-events: none; + } + + &:after { + content: ""; + width: 200px; + height: 100%; + position: absolute; + right: -2px; + top: 0; + background: linear-gradient( + 272deg, + #0d0817 20%, + rgba(15, 10, 22, 0) 99.21% + ); + z-index: 10; + pointer-events: none; + } + } +} + +.Card { + $height: 320px; + + position: relative; + padding: 8px; + min-height: $height; + + @include breakpoint(lg) { + min-height: auto; + } + + img { + border-radius: 16px; + } + + .CardInner { + padding: 24px; + display: block; + border-radius: 8px; + border: 1px solid var(--gradient-6, #504d61); + background: var(--Neutrals-Grey-500, #1d1a23); + min-height: 300px; + position: relative; + + @include breakpoint(sm) { + min-height: 320px; + } + + @include breakpoint(md) { + min-height: 350px; + } + + @include breakpoint(lg) { + padding: 0; + border-radius: 16px; + box-shadow: none !important; + min-height: 0; + } + } + + .CardImageWrapper { + position: relative; + aspect-ratio: 14 / 9; + width: 100%; + height: auto; + } + + .CardContent { + @include breakpoint(lg) { + display: none; + } + } +} + +.TooltipContainer { + position: absolute; + top: 0; + left: 0; + width: calc(100%); + z-index: 12; +} + +.Tooltip { + display: none !important; + + @include breakpoint(lg) { + display: block !important; + } + + position: absolute; + top: -24px; + left: -24px; + width: calc(100% + 48px); + padding: 24px; + border-radius: 16px; + z-index: 10 !important; + border-radius: 8px; + border: 1px solid var(--gradient-6, #504d61); + background: var(--Neutrals-Grey-500, #1d1a23); +} diff --git a/src/components/solutions/gaming/GamingSlider.tsx b/src/components/solutions/gaming/GamingSlider.tsx new file mode 100644 index 00000000..5bcc5a0f --- /dev/null +++ b/src/components/solutions/gaming/GamingSlider.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useTranslation } from "next-i18next"; +import Image from "next/image"; +import Slider from "react-slick"; +import "slick-carousel/slick/slick.css"; +import "slick-carousel/slick/slick-theme.css"; + +import styles from "./GamingSlider.module.scss"; +import CaretIcon from "@/components/icons/Caret"; + +const GamingSlider = () => { + const { t } = useTranslation(); + const [tooltip, setTooltip] = useState<{ + title: string; + content: string; + img: string; + url: string; + boxShadow: string; + } | null>(null); + + const handleMouseEnter = (card: { + title: string; + content: string; + img: string; + url: string; + boxShadow: string; + }) => { + console.log("Hovering over card:", card); + setTooltip(card); + }; + + const handleMouseLeave = () => { + console.log("Mouse left card"); + setTooltip(null); + }; + + const cards = [ + { + title: t("solutions-gaming.cards.star-atlas.title"), + content: t("solutions-gaming.cards.star-atlas.content"), + img: "/solutions/gaming/slider/row1-card1.jpg", + url: "https://play.staratlas.com/", + boxShadow: "#3079A6B2", + }, + { + title: t("solutions-gaming.cards.aurory.title"), + content: t("solutions-gaming.cards.aurory.content"), + img: "/solutions/gaming/slider/row1-card2.jpg", + url: "https://aurory.io/", + boxShadow: "#AE7272B2", + }, + { + title: t("solutions-gaming.cards.stepn-go.title"), + content: t("solutions-gaming.cards.stepn-go.content"), + img: "/solutions/gaming/slider/row1-card3.jpg", + url: "https://stepngo.com/", + boxShadow: "#7E5CDCB2", + }, + { + title: t("solutions-gaming.cards.br1.title"), + content: t("solutions-gaming.cards.br1.content"), + img: "/solutions/gaming/slider/row1-card4.jpg", + url: "https://www.br1game.com/", + boxShadow: "#22ABC1B2", + }, + { + title: t("solutions-gaming.cards.nyan-heros.title"), + content: t("solutions-gaming.cards.nyan-heros.content"), + img: "/solutions/gaming/slider/row1-card5.jpg", + url: "https://nyanheroes.com/", + boxShadow: "#3079A6B2", + }, + { + title: t("solutions-gaming.cards.photo-finish-live.title"), + content: t("solutions-gaming.cards.photo-finish-live.content"), + img: "/solutions/gaming/slider/row2-card1.jpg", + url: "https://photofinish.live/", + boxShadow: "#206E72B2", + }, + { + title: t("solutions-gaming.cards.genopets.title"), + content: t("solutions-gaming.cards.genopets.content"), + img: "/solutions/gaming/slider/row2-card2.jpg", + url: "https://www.genopets.me/", + boxShadow: "#A1937DB2", + }, + { + title: t("solutions-gaming.cards.portals.title"), + content: t("solutions-gaming.cards.portals.content"), + img: "/solutions/gaming/slider/row2-card3.jpg", + url: "https://theportal.to/", + boxShadow: "#29729FB2", + }, + { + title: t("solutions-gaming.cards.low-life-forms.title"), + content: t("solutions-gaming.cards.low-life-forms.content"), + img: "/solutions/gaming/slider/row2-card4.jpg", + url: "https://www.rtrigger.com/#lowlifeforms", + boxShadow: "#9F51EBB2", + }, + { + title: t("solutions-gaming.cards.mixmob.title"), + content: t("solutions-gaming.cards.mixmob.content"), + img: "/solutions/gaming/slider/row2-card5.jpg", + url: "https://uprising.mixmob.io/", + boxShadow: "#2D42C3B2", + }, + ]; + + const settings = { + className: "center", + centerMode: true, + infinite: true, + centerPadding: "140px", + arrows: true, + slidesToShow: 3, + speed: 500, + rows: 2, + slidesPerRow: 1, + responsive: [ + { + breakpoint: 992, + settings: { + slidesToShow: 2, + centerPadding: "48px", + }, + }, + { + breakpoint: 639, + settings: { + slidesToShow: 1, + centerPadding: "56px", + }, + }, + { + breakpoint: 480, + settings: { + slidesToShow: 1, + centerPadding: "32px", + }, + }, + ], + }; + + return ( +
    + + {cards.map((card, index) => ( +
    handleMouseEnter(card)} + onMouseLeave={handleMouseLeave} + > + +
    + {card.title} +
    +
    +
    + {card.title} + +
    +

    {card.content}

    +
    + + + {tooltip && tooltip.title === card.title && ( + +
    + {tooltip.title} +
    +
    +
    + {tooltip.title} + +
    +

    {tooltip.content}

    +
    + + )} +
    + ))} +
    +
    + ); +}; + +export default GamingSlider; diff --git a/src/components/solutions/gaming/GamingVideoHero.jsx b/src/components/solutions/gaming/GamingVideoHero.jsx new file mode 100644 index 00000000..8e02d42a --- /dev/null +++ b/src/components/solutions/gaming/GamingVideoHero.jsx @@ -0,0 +1,119 @@ +import { useRef, useEffect } from "react"; +import { useTranslation } from "next-i18next"; +import useReducedMotion from "../../../hooks/useReducedMotion"; + +import Button from "@/components/solutions/Button"; +import { OpacityInText } from "@/components/shared/Text"; +import { MotionSlideIn } from "@/components/shared/Motions"; + +import styles from "./GamingVideoHero.module.scss"; + +const GamingVideoHero = () => { + const { t } = useTranslation(); + const video1Ref = useRef(null); + const video2Ref = useRef(null); + const [prefersReducedMotion] = useReducedMotion(); + + const videos = [ + { + src_720: + "https://player.vimeo.com/progressive_redirect/playback/1009457872/rendition/720p/file.mp4?loc=external&signature=9c3fab67abf9c4a84a9db0c3cc16ffaeab06ad83e2c8100aa5b524d0184f9954", + src_1080: + "https://player.vimeo.com/progressive_redirect/playback/1009457872/rendition/1080p/file.mp4?loc=external&signature=86bd2ccd9bc532bff002c5e4eeb2cd892842aba7ac17b8f06936c5bfeab95012", + poster: "/solutions/gaming/hero-video-1.webp", + }, + { + src_720: + "https://player.vimeo.com/progressive_redirect/playback/1009457866/rendition/720p/file.mp4?loc=external&signature=f945644cad9795580cb0b90ce6d3ca744fdd13a5faf7eeafd1fb35b46739a7bd", + src_1080: + "https://player.vimeo.com/progressive_redirect/playback/1009457866/rendition/1080p/file.mp4?loc=external&signature=f77cc386fce4caa083968ed91504145a93b448a51d4b8e22b2d444484722d128", + poster: "/solutions/gaming/hero-video-2.webp", + }, + ]; + + const handleVideoEnd = () => { + video1Ref.current.style.opacity = 0; + video2Ref.current.style.opacity = 1; + }; + + useEffect(() => { + if (video1Ref.current && prefersReducedMotion) { + video1Ref.current.pause(); + } + }, [prefersReducedMotion]); + + useEffect(() => { + if (video1Ref.current && video2Ref.current) { + video1Ref.current.play(); + video2Ref.current.play(); + } + }, [video1Ref, video2Ref]); + + return ( +
    +
    + + + + +
    +
    + +
    + + {t("solutions-gaming.hero.kicker")} + + + {t("solutions-gaming.hero.title")} + + + {t("solutions-gaming.hero.subtitle")} + + + +
    +
    +
    +
    +
    + ); +}; + +export default GamingVideoHero; diff --git a/src/components/solutions/gaming/GamingVideoHero.module.scss b/src/components/solutions/gaming/GamingVideoHero.module.scss new file mode 100644 index 00000000..fdde4f21 --- /dev/null +++ b/src/components/solutions/gaming/GamingVideoHero.module.scss @@ -0,0 +1,116 @@ +@import "~@/scss/solutions/_variables.scss"; + +.GamingVideoHero { + height: calc(100vh - 76px); + position: relative; + + video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + } + + .Video2 { + opacity: 0; + } +} + +.VideoWrapper { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + .VideoOverlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.4); + z-index: 1; + } +} + +.TextBlock { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 2; + text-align: center; + width: 100%; + padding: 24px; + + .Kicker { + font-size: 16px; + font-weight: 800; + line-height: 1.25; + letter-spacing: -0.02em; + text-align: center; + text-transform: uppercase; + margin: 0 0 24px; + display: block; + + @include breakpoint(lg) { + font-size: 22px; + font-weight: 800; + line-height: 1.25; + letter-spacing: -0.02em; + } + } + + h1 { + font-size: 48px; + font-weight: 700; + line-height: 1; + letter-spacing: -0.02em; + } + + p { + font-size: 20px; + font-weight: 700; + line-height: 1.2; + margin: 24px auto 0; + max-width: 702px; + } +} + +.Buttons { + display: flex; + flex-direction: column; + width: max-content; + margin: 24px auto 0; + gap: 16px; + + @media (max-width: #{$screen-md-min - 1}) { + a { + font-size: 15px; + line-height: 1.32; + } + } + + @include breakpoint(lg) { + display: grid; + grid-template-columns: 1fr 1fr; + } +} + +@include breakpoint(xl) { + .TextBlock { + h1 { + font-size: 96px; + } + + p { + font-size: 28px; + line-height: 1.15; + letter-spacing: -0.02em; + } + } +} diff --git a/src/components/solutions/gaming/TVMert.module.scss b/src/components/solutions/gaming/TVMert.module.scss new file mode 100644 index 00000000..2281efca --- /dev/null +++ b/src/components/solutions/gaming/TVMert.module.scss @@ -0,0 +1,79 @@ +@import "../../../scss/solutions/_variables.scss"; + +.TVMert { + position: relative; + padding: 64px 0; + + .Title { + font-size: 40px; + font-weight: 700; + line-height: 1.06; + letter-spacing: -0.01em; + text-align: center; + padding: 0 24px; + margin: 0 auto 24px; + position: relative; + z-index: 2; + + --gradient: linear-gradient( + 90deg, + #3f37c9 4.6%, + #46dcf8 31.37%, + #ffffff 50.45% + ); + } +} + +.VideoWrapper { + position: relative; + width: 100vw; + height: calc(100vw * 0.5); + transform: scale(1.25); + + @include breakpoint(sm) { + transform: scale(1); + } + + @include breakpoint(md) { + margin-top: -5%; + position: relative; + z-index: 1; + } + + video { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + width: 100%; + height: 100%; + object-fit: contain; + object-position: center; + opacity: 0; + transition: opacity $duration-standard; + + &.Active { + opacity: 1; + } + } +} + +@include breakpoint(md) { + .TVMert { + .Title { + font-size: 80px; + margin-bottom: 0; + letter-spacing: -0.03em; + } + } + + .VideoWrapper { + width: 100%; + } +} + +@include breakpoint(xl) { + .TVMert { + padding: 128px 0 0; + } +} diff --git a/src/components/solutions/gaming/TVMert.tsx b/src/components/solutions/gaming/TVMert.tsx new file mode 100644 index 00000000..c03f0e74 --- /dev/null +++ b/src/components/solutions/gaming/TVMert.tsx @@ -0,0 +1,106 @@ +import { useRef, useEffect } from "react"; +import { Trans } from "next-i18next"; +import classNames from "classnames"; + +import { AnimatedText, GradientText } from "@/components/shared/Text"; + +import useReducedMotion from "@/hooks/useReducedMotion"; + +import styles from "./TVMert.module.scss"; + +const TVMert = () => { + const video1Ref = useRef(null); + const video2Ref = useRef(null); + const [prefersReducedMotion] = useReducedMotion(); + + const videos = [ + { + src_720: + "https://player.vimeo.com/progressive_redirect/playback/1009649838/rendition/720p/file.mp4?loc=external&signature=0c0ffaab9ea401d986555187c28100a88594ccc69d8317f295f06cb618815e4c", + src_1080: + "https://player.vimeo.com/progressive_redirect/playback/1009649838/rendition/1080p/file.mp4?loc=external&signature=8769962746274f024137d46578b7ee5630fe33ea5d845d5ee8e342697ab82dac", + poster: "/solutions/gaming/mert-tv-1.jpeg", + }, + { + src_720: + "https://player.vimeo.com/progressive_redirect/playback/1009649823/rendition/720p/file.mp4?loc=external&signature=9917c37382359cac1733ee757060f86b07cff29c8d646842962d56321c52d544", + src_1080: + "https://player.vimeo.com/progressive_redirect/playback/1009649823/rendition/1080p/file.mp4?loc=external&signature=b8f8a4574f9ee500b74e63279e83c0da87258c7c306f0ba879d051f264f85f10", + poster: "/solutions/gaming/mert-tv-2.jpeg", + }, + ]; + + const handleVideoEnd = () => { + video1Ref.current.style.opacity = 0; + video2Ref.current.style.opacity = 1; + }; + + useEffect(() => { + if (video1Ref.current && prefersReducedMotion) { + video1Ref.current.pause(); + } + }, [prefersReducedMotion, video1Ref]); + + useEffect(() => { + if (video1Ref.current && video2Ref.current) { + video1Ref.current.play(); + video2Ref.current.play(); + setTimeout(() => { + video1Ref.current?.classList.add(styles.Active); + }, 500); + } + }, [video1Ref, video2Ref]); + + return ( +
    + + , + }} + /> + + +
    + + + +
    +
    + ); +}; + +export default TVMert; diff --git a/src/components/solutions/layout.tsx b/src/components/solutions/layout.tsx new file mode 100644 index 00000000..f8a7406e --- /dev/null +++ b/src/components/solutions/layout.tsx @@ -0,0 +1,21 @@ +import { ReactNode } from "react"; +import classNames from "classnames"; +import Header from "../Header"; +import Footer from "../Footer"; +import styles from "./Layout.module.scss"; + +interface LayoutProps { + headerClassName?: string; + children: ReactNode; +} + +const Layout = ({ headerClassName, children }: LayoutProps) => { + return ( +
    +
    +
    {children}
    +
    +
    + ); +}; +export default Layout; diff --git a/src/components/solutions/loyalty/LoyaltyHero.jsx b/src/components/solutions/loyalty/LoyaltyHero.jsx new file mode 100644 index 00000000..e7a81283 --- /dev/null +++ b/src/components/solutions/loyalty/LoyaltyHero.jsx @@ -0,0 +1,53 @@ +import Lottie from "react-lottie"; +import { useTranslation } from "next-i18next"; + +import Text from "@/components/shared/Text"; +import Button from "@/components/solutions/Button"; +import { MotionSlideIn } from "@/components/shared/Motions"; + +import styles from "./LoyaltyHero.module.scss"; + +const LoyaltyHero = ({ heroLottie }) => { + const { t } = useTranslation(); + + return ( +
    +
    + + {t("solutions-loyalty.hero.eyebrow")} + + + {t("solutions-loyalty.hero.title")} + + + {t("solutions-loyalty.hero.subtitle")} + + +
    +
    +
    + + + + +
    + ); +}; + +export default LoyaltyHero; diff --git a/src/components/solutions/loyalty/LoyaltyHero.module.scss b/src/components/solutions/loyalty/LoyaltyHero.module.scss new file mode 100644 index 00000000..53b4078f --- /dev/null +++ b/src/components/solutions/loyalty/LoyaltyHero.module.scss @@ -0,0 +1,114 @@ +@import "@/scss/solutions/_variables.scss"; + +.Hero { + padding: 64px 24px; + + .HeroTitle { + font-size: 36px; + font-weight: 700; + line-height: 1.14; + letter-spacing: -0.01em; + text-align: center; + text-wrap: initial; + } + + .HeroSubtitle { + font-size: 20px; + font-weight: 700; + line-height: 1.12; + text-align: center; + color: #a2a1b2; + margin: 24px 0 40px; + max-width: 100%; + } + + .Eyebrow { + color: #64a8f2; + text-align: center; + margin-bottom: 24px; + text-transform: uppercase; + font-size: 16px; + font-weight: 800; + line-height: 1.25; + letter-spacing: -0.02em; + } + + .ButtonWrapper { + display: flex; + flex-direction: column; + width: max-content; + margin: 24px auto 40px; + gap: 16px; + + @media (max-width: #{$screen-xl-min - 1}) { + a { + font-size: 15px; + line-height: 1.32; + } + } + + @include breakpoint(lg) { + display: grid; + grid-template-columns: 1fr 1fr; + margin-left: 0; + + a { + width: 100%; + flex-direction: column; + } + } + } + + @include breakpoint(lg) { + display: flex; + gap: 80px; + max-width: 1080px; + margin: 0 auto; + align-items: center; + padding: 128px 40px; + box-sizing: content-box; + + * { + flex-basis: 50%; + } + + .HeroTitle { + font-size: 64px; + text-align: left; + } + + .HeroSubtitle { + font-size: 24px; + text-align: left; + } + + .Eyebrow { + text-align: left; + } + } + + @include breakpoint(xl) { + .HeroTitle { + font-size: 64px; + font-weight: 700; + line-height: 1.04; + letter-spacing: -0.02em; + text-align: left; + max-width: 400px; + } + + .HeroSubtitle { + font-size: 24px; + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.02em; + } + + .Eyebrow { + font-size: 20px; + font-weight: 800; + line-height: 1.25; + letter-spacing: -0.44px; + } + } +} diff --git a/src/components/solutions/token-extensions/EcosystemToggle.module.scss b/src/components/solutions/token-extensions/EcosystemToggle.module.scss new file mode 100644 index 00000000..060c700b --- /dev/null +++ b/src/components/solutions/token-extensions/EcosystemToggle.module.scss @@ -0,0 +1,326 @@ +@import "~@/scss/solutions/_variables.scss"; + +.TitleBlock { + padding-top: 64px; + + @include breakpoint(lg) { + padding-top: 80px; + } +} + +.Title { + font-size: 40px; + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.03em; + text-align: center; + color: var(--white); + text-transform: capitalize; + margin: 0; + padding: 0 24px; + + strong { + background: var(--gradient-2); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + } + + @include breakpoint(lg) { + font-size: 72px; + line-height: 1.08; + letter-spacing: -0.03em; + margin-bottom: 24px; + } +} + +.Text { + font-size: 20px; + font-weight: 700; + line-height: 1.12; + letter-spacing: -0.02em; + text-align: center; + color: var(--grey-250); + max-width: 698px; + padding: 0 24px; + margin: 24px auto 0; + + @include breakpoint(md) { + font-size: 24px; + } + + @include breakpoint(lg) { + font-size: 32px; + font-weight: 500; + line-height: 1.25; + text-align: center; + max-width: 886px; + } +} + +.MainCollapsible { + & > button[data-state="open"] { + animation: hide 300ms ease; + visibility: hidden; + transition: visibility 300ms ease; + } +} + +.CollapsibleContentWrapper { + margin-top: 64px; + margin-bottom: 64px; + + @include breakpoint(lg) { + margin-top: 124px; + margin-bottom: 80px; + } + + .ToggleBtn { + background: var(--grey-450); + font-size: 15px; + color: white; + line-height: 1.3; + font-weight: 700; + padding: 12px; + border-radius: 8px; + margin-bottom: 24px; + + svg path { + stroke: var(--white); + } + + @include breakpoint(md) { + font-size: 16px; + justify-content: center; + } + + @include breakpoint(lg) { + justify-content: center; + } + } + + .EcosystemPreviewContent { + position: relative; + // overflow: hidden; + + &:after { + content: ""; + display: block; + height: 100%; + width: 100%; + position: absolute; + top: 0; + opacity: 1; + background: linear-gradient(0deg, #0f0a16 0%, rgba(15, 10, 22, 0.6) 100%); + transform: translateY(0%); + transition: + opacity 0.3s ease-in-out, + background 0.5s ease-in-out, + transform 0s ease-in-out; + } + } + + div[data-state="closed"] { + .EcosystemPreviewContent { + &:after { + opacity: 1; + background: linear-gradient( + 0deg, + #0f0a16 0%, + rgba(15, 10, 22, 0.6) 100% + ); + transform: translateY(0%); + } + } + } + + div[data-state="open"] { + .EcosystemPreviewContent { + &:after { + opacity: 0; + background: transparent; + transform: translateY(-100%); + } + } + } + + .EcosystemItem { + position: relative; + // .AccordionContent { + // opacity: 0; + // } + + .AccordionContent[data-state="closed"] { + animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1); + } + + .AccordionContent[data-state="open"] { + animation: slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1) forwards; + + @include breakpoint(lg) { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: auto; + } + } + + h3 { + margin: 0; + line-height: 1; + display: flex; + } + + button { + color: var(--grey-400); + letter-spacing: -0.01em; + font-size: 18px; + line-height: 1.16; + padding: 6px 0; + width: 100%; + + @include breakpoint(md) { + font-size: 20px; + } + + @include breakpoint(lg) { + font-size: 28px; + } + + svg { + display: none; + } + + &[data-state="open"] { + color: var(--white); + } + } + + .ItemToggleBtn { + display: flex; + justify-content: center; + + &:hover { + color: var(--white); + transition: color 0.1s ease-in; + } + + @include breakpoint(lg) { + justify-content: flex-start; + } + } + + .EcosystemItemContent { + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + text-align: center; + padding: 6px; + position: relative; + + @include breakpoint(lg) { + align-items: flex-end; + text-align: left; + } + + .EcosystemItemContentInner { + display: flex; + flex-direction: column; + gap: 8px; + + @include breakpoint(lg) { + width: 329px; + gap: 40px; + position: relative; + } + } + + h4 { + font-size: 13px; + line-height: 1.32; + color: var(--grey-100); + margin: 0; + font-weight: bold; + + @include breakpoint(md) { + font-size: 15px; + } + + @include breakpoint(lg) { + font-size: 18px; + } + } + + p { + font-size: 13px; + line-height: 1.32; + color: var(--grey-300); + margin: 0; + font-weight: bold; + width: 75%; + max-width: 500px; + margin: 0 auto; + + @include breakpoint(md) { + font-size: 15px; + } + + @include breakpoint(lg) { + max-width: 100%; + width: 100%; + margin: 0 auto; + font-size: 18px; + } + } + + a { + font-size: 13px; + line-height: 1.1; + color: var(--white); + max-width: 75%; + margin: 8px auto; + + @include breakpoint(md) { + font-size: 15px; + } + + @include breakpoint(lg) { + max-width: 100%; + width: 100%; + margin: 0 auto; + } + } + } + } +} + +@keyframes hide { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes slideDown { + from { + height: 0; + opacity: 0; + } + to { + height: var(--radix-accordion-content-height); + opacity: 1; + } +} + +@keyframes slideUp { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } +} diff --git a/src/components/solutions/token-extensions/EcosystemToggle.tsx b/src/components/solutions/token-extensions/EcosystemToggle.tsx new file mode 100644 index 00000000..3330d7aa --- /dev/null +++ b/src/components/solutions/token-extensions/EcosystemToggle.tsx @@ -0,0 +1,133 @@ +import { ReactNode, Fragment } from "react"; +import classNames from "classnames"; +import { Trans } from "next-i18next"; +import * as Accordion from "@radix-ui/react-accordion"; + +import Text, { AnimatedText, GradientText } from "@/components/shared/Text"; +import CollapsibleContent from "@/components/shared/CollapsibleContent"; + +import styles from "./EcosystemToggle.module.scss"; + +interface EcosystemItem { + label: string; + content: ReactNode; +} + +const AccordionItem = ({ value, label, content }) => { + return ( + + + + {label} + + + + + {content} + + + ); +}; + +const EcosystemToggle = ({ titleKey, textKey, toggleLabel, items }) => { + return ( +
    +
    + {titleKey && ( + + + ), + }} + /> + + )} + + {textKey && ( + + + + )} +
    + +
    + + + {items.slice(0, 3).map((item: EcosystemItem, index: number) => ( + + + + ))} +
    + } + > + {items + .slice(3) + .map( + ( + item: { label: string; content?: ReactNode }, + index: number, + ) => ( + + + + ), + )} + {/* {items + .slice(3) + .map( + ( + item: { label: string; content?: ReactNode }, + index: number, + ) => ( + + + + ), + )} */} + + +
    +
    + ); +}; + +export const EcosystemItemContentWrap = ({ + children, +}: { + children: ReactNode; +}) => ( +
    +
    {children}
    +
    +); + +export const EcosystemItemContentTitle = ({ text }: { text: string }) => ( + {text} +); + +export const EcosystemItemContentText = ({ text }: { text: string }) => ( + {text} +); + +export default EcosystemToggle; diff --git a/src/components/solutions/wallets-explorer/CollapsibleContent.jsx b/src/components/solutions/wallets-explorer/CollapsibleContent.jsx new file mode 100644 index 00000000..4f6da03c --- /dev/null +++ b/src/components/solutions/wallets-explorer/CollapsibleContent.jsx @@ -0,0 +1,43 @@ +import { useState } from "react"; +import * as Collapsible from "@radix-ui/react-collapsible"; +import classNames from "classnames"; + +import CaretIcon from "@/components/icons/Caret"; + +import styles from "./CollapsibleContent.module.scss"; + +const CollapsibleContent = ({ + label, + defaultOpen, + children, + icon, + classes, +}) => { + const [open, setOpen] = useState(defaultOpen || false); + + return ( + + {label && ( + + + + )} + + {children} + + ); +}; + +export default CollapsibleContent; diff --git a/src/components/solutions/wallets-explorer/CollapsibleContent.module.scss b/src/components/solutions/wallets-explorer/CollapsibleContent.module.scss new file mode 100644 index 00000000..0e473028 --- /dev/null +++ b/src/components/solutions/wallets-explorer/CollapsibleContent.module.scss @@ -0,0 +1,23 @@ +.CollapsibleTrigger { + border-bottom: 1px solid var(--grey-400); + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.Label { + font-size: 13px; + font-weight: 400; + line-height: 1.4; + letter-spacing: 0.02em; + color: var(--blue); + text-transform: uppercase; + padding: 19px 0; + margin: 0; + text-align: left; +} + +.IconArrowDown { + transform: rotate(180deg); +} diff --git a/src/components/solutions/wallets-explorer/Filters.jsx b/src/components/solutions/wallets-explorer/Filters.jsx new file mode 100644 index 00000000..da66b5f6 --- /dev/null +++ b/src/components/solutions/wallets-explorer/Filters.jsx @@ -0,0 +1,433 @@ +import { useState } from "react"; +import Image from "next/image"; +import { useTranslation } from "next-i18next"; + +import CollapsibleContent from "@/components/solutions/wallets-explorer/CollapsibleContent"; + +import XIcon from "../../../../assets/wallets/x.inline.svg"; + +import styles from "./Filters.module.scss"; + +const CloseButton = ({ onClick }) => { + return ( + + ); +}; + +const ToggleFormField = ({ key, label, id, checked, onChange }) => ( +
    +

    + {label} +

    +
    + + +
    +
    +); + +export const MobileFilters = ({ + filterState, + toggleFilterActiveState, + resetWalletsAndFilters, +}) => { + const { t } = useTranslation(); + + const [open, setOpen] = useState(false); + + // Active filters count + const activeFiltersCount = filterState.filter( + (filter) => filter.checked, + ).length; + + // Categories + const beginnerFilters = filterState.filter( + (filter) => filter.category === "beginner", + ); + const advancedFilters = filterState.filter( + (filter) => filter.category === "advanced", + ); + + // Advanced filters are further categorized into subcategories + const walletTypeFilters = advancedFilters.filter( + (filter) => filter.subCategory === "wallet_type", + ); + const financialTransactionFilters = advancedFilters.filter( + (filter) => filter.subCategory === "financial_transaction", + ); + const assetManagementFilters = advancedFilters.filter( + (filter) => filter.subCategory === "asset_management", + ); + const securityRecoveryFilters = advancedFilters.filter( + (filter) => filter.subCategory === "security_recovery", + ); + const toolingFilters = advancedFilters.filter( + (filter) => filter.subCategory === "tooling", + ); + + const handleMenuToggle = () => { + !open + ? (document.body.style.overflow = "hidden") + : (document.body.style.overflow = "auto"); + setOpen(!open); + }; + + return ( +
    + + +
    +
    +

    + {t("solutions-wallets-explorer.filters")} +

    + +
    + +
    +
    +
    + + {beginnerFilters.length && + beginnerFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + +
    + +
    + + + {walletTypeFilters.length && + walletTypeFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + + + + {financialTransactionFilters.length && + financialTransactionFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + + + + {assetManagementFilters.length && + assetManagementFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + + + + {securityRecoveryFilters.length && + securityRecoveryFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + + + + {toolingFilters.length && + toolingFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + + +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    + ); +}; + +export const DesktopFilters = ({ + filterState, + toggleFilterActiveState, + resetWalletsAndFilters, +}) => { + const { t } = useTranslation(); + + // Active filters count + const activeFiltersCount = filterState.filter( + (filter) => filter.checked, + ).length; + + // Categories + const beginnerFilters = filterState.filter( + (filter) => filter.category === "beginner", + ); + const advancedFilters = filterState.filter( + (filter) => filter.category === "advanced", + ); + + // Advanced filters are further categorized into subcategories + const walletTypeFilters = advancedFilters.filter( + (filter) => filter.subCategory === "wallet_type", + ); + const financialTransactionFilters = advancedFilters.filter( + (filter) => filter.subCategory === "financial_transaction", + ); + const assetManagementFilters = advancedFilters.filter( + (filter) => filter.subCategory === "asset_management", + ); + const securityRecoveryFilters = advancedFilters.filter( + (filter) => filter.subCategory === "security_recovery", + ); + const toolingFilters = advancedFilters.filter( + (filter) => filter.subCategory === "tooling", + ); + + return ( +
    +
    +
    +

    + {t("solutions-wallets-explorer.filters")} ({activeFiltersCount}) +

    + +
    + +
    +
    +
    + + {beginnerFilters.length && + beginnerFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + +
    + +
    + + + } + classes={styles.FirstChild} + > + {walletTypeFilters.length && + walletTypeFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + + + + } + classes={styles.Child} + > + {financialTransactionFilters.length && + financialTransactionFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + + + + } + classes={styles.Child} + > + {assetManagementFilters.length && + assetManagementFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + + + + } + classes={styles.Child} + > + {securityRecoveryFilters.length && + securityRecoveryFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + + + + } + classes={styles.Child} + > + {toolingFilters.length && + toolingFilters.map((filter, index) => ( + toggleFilterActiveState(filter.id)} + /> + ))} + + +
    +
    +
    +
    +
    + ); +}; diff --git a/src/components/solutions/wallets-explorer/Filters.module.scss b/src/components/solutions/wallets-explorer/Filters.module.scss new file mode 100644 index 00000000..04ded05b --- /dev/null +++ b/src/components/solutions/wallets-explorer/Filters.module.scss @@ -0,0 +1,346 @@ +@import "~@/scss/solutions/_variables.scss"; + +.Fieldsets { + display: flex; + flex-direction: column; + gap: 15px; + padding-bottom: 24px; +} + +.FieldsetsInner { + padding-bottom: 96px; + padding-top: 105px; +} + +.FormField { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + width: 100%; + gap: 5px; + padding: 19px 0; + position: relative; +} + +.FormFieldLabel { + font-size: 13px; + font-weight: 400; + line-height: 1.13; + letter-spacing: 0.02em; + text-align: left; + color: #a2a1b2; + text-transform: uppercase; + margin: 0; +} + +.LabelChecked { + color: #fff !important; +} + +.SwitchWrapper { + display: flex; + align-items: center; + position: relative; +} +.Switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + background-color: transparent; + border-radius: 20px; + transition: + background 0.3s, + border 0.3s, + left 0.3s; + border: 1px solid #6c6a81; + cursor: pointer; + + &:after { + content: ""; + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: transparent; + top: 1px; + left: 1px; + transition: all 0.3s; + border: 1px solid #a2a1b2; + } +} + +.Checkbox:checked + .Switch::after { + left: 21px; + background-color: #14f195; + border: 1px solid #14f195; +} +.Checkbox:checked + .Switch { + border: 1px solid #14f195; +} +.Checkbox { + appearance: none; + width: 100%; + height: 100%; + position: absolute; + top: 0; + z-index: 2; + cursor: pointer; +} + +// Mobile +.MobileFiltersMenuToggle { + background: #322f43; + width: 100%; + padding: 8px; + border-radius: 8px; + font-size: 15px; + font-weight: 700; + line-height: 1.32; +} + +.MobileFiltersMenu { + display: none; + height: 100%; + min-height: 100vh; + overflow: scroll; // TODO: body overflow hidden to prevent double scrollbar + box-sizing: content-box; + + // Menu Open State + &[data-open="true"] { + display: block; + position: fixed; + top: 0; + left: 0; + background: #1d1a23; + z-index: 1021; + padding: 0 24px; + width: calc(100% - 48px); // 48px = x-axis padding + } + + header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 56px 0 24px; + position: fixed; + top: 0; + width: calc(100% - 48px); + background: #1d1a23; + z-index: 2; + } + + .HeaderTitle { + font-size: 18px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.01em; + text-align: left; + margin: 0; + } + + .Actions { + padding: 24px 0; + position: fixed; + bottom: 0; + width: 100%; + left: 0; + background: #1d1a23; + + .ActionsInner { + padding: 0 24px; + display: flex; + gap: 8px; + } + } + + .ActionButton { + width: 100%; + cursor: pointer; + background: #322f43; + border-radius: 8px; + font-size: 15px; + font-weight: 700; + line-height: 1.32; + text-align: center; + padding: 14px; + } +} + +.MobileFilters { + margin-bottom: 40px; + + @include breakpoint(lg) { + display: none; + margin-bottom: 0; + } +} + +.DesktopFilters { + display: none; + + @include breakpoint(lg) { + display: block; + } + + .FormField { + padding: 16px 0; + } + .DesktopFieldsetOne { + background: var(--grey-500); + } + + button { + p { + align-items: center; + padding-top: 24px; + } + } +} + +.DesktopFiltersHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding-left: 16px; + padding-bottom: 16px; +} + +.DesktopActionButton { + font-family: "ABC Mono"; + color: #a2a1b2; + font-size: 15px; + line-height: 1.15; + letter-spacing: 0.3px; + text-transform: uppercase; + border-bottom: 1px solid #a2a1b2; + margin: 0; + padding: 0; + transition: color 0.2s ease; + + &:hover { + color: #9945ff; + } +} + +.DesktopFiltersCount { + font-size: 18px; + font-weight: 700; + line-height: 1.16; + letter-spacing: -0.18px; + margin: 0; + padding: 0; +} + +.DesktopFieldsets { + margin-top: 24px; + + p { + color: #a2a1b2; + font-family: var(--font-mono); + font-size: 15px; + font-weight: 400; + line-height: 1.15; + letter-spacing: 0.02em; + text-transform: uppercase; + } + + button { + padding-top: 8px; + } +} + +.DesktopFieldsetOne { + border-radius: 8px; + background: #1d1a23; + padding: 16px; + + p { + padding: 0; + } +} + +.DesktopFieldsetTwo { + border-radius: 8px; + background: #1d1a23; + padding: 0; + margin-top: 24px; + + button { + border: none; + } +} + +.Parent { + & > button { + padding: 0 16px; + p, + svg { + color: #64a8f2; + } + svg path { + stroke: #64a8f2; + } + svg { + padding: 6px; + box-sizing: content-box; + } + } +} + +.FirstChild { + border-top: 1px solid #504d61; + padding-bottom: 8px; + + button { + p, + svg { + color: #80ecff; + } + } + + svg path { + stroke: #80ecff; + } + + p { + display: flex; + align-items: flex-start; + gap: 6px; + + svg { + margin-bottom: 4px; + } + } + + & > div, + button { + padding: 0 32px; + } +} + +.Child { + padding: 0 16px; + padding-bottom: 8px; + + > * { + padding: 0 16px; + } + + svg path { + stroke: #80ecff; + } + + button { + border-top: 1px solid #504d61; + p, + svg { + color: #80ecff; + } + + p { + display: flex; + gap: 6px; + } + } +} diff --git a/src/components/solutions/wallets-explorer/WalletCard.jsx b/src/components/solutions/wallets-explorer/WalletCard.jsx new file mode 100644 index 00000000..d8d2d375 --- /dev/null +++ b/src/components/solutions/wallets-explorer/WalletCard.jsx @@ -0,0 +1,307 @@ +import classNames from "classnames"; +import { useState } from "react"; +import Link from "next/link"; +import * as Collapsible from "@radix-ui/react-collapsible"; +import Image from "next/image"; +import { useTranslation } from "next-i18next"; + +import CaretIcon from "@/components/icons/Caret"; + +import styles from "./WalletCard.module.scss"; + +import ArrowUpRightIcon from "../../../../assets/wallets/arrow-up-right.inline.svg"; + +// Function to group keys into categories +const groupWalletData = (content) => { + const groups = { + "Wallet Type": ["custodial", "non_custodial", "hardware", "mpc"], + "Financial Transactions": ["buy_crypto", "sell_crypto", "staking"], + "Asset Management": ["hold_nfts", "gas_abstraction", "spending_limits"], + "Security and Recovery": [ + "social_recovery", + "open_source", + "private_key_infrastructure", + ], + "Solana Tooling": ["te", "blinks_and_actions", "solana_pay"], + }; + + // Generate content for each group by filtering true values + const groupedContent = Object.entries(groups).map(([groupName, keys]) => { + const filteredKeys = keys.filter((key) => content[key] === true); + return { + groupName, + values: filteredKeys, + }; + }); + + return groupedContent; +}; + +const Tag = ({ text }) => ( +
    + {text} +
    +); + +const Dot = () => ; + +const WalletCard = ({ + index, + walletImage, + name, + websiteUrl, + newToCrypto, + developer, + content, +}) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(true); + + const tags = []; + if (newToCrypto) tags.push(t("solutions-wallets-explorer.tags.beginner")); + if (developer) tags.push(t("solutions-wallets-explorer.tags.developer")); + + const groupedContent = groupWalletData(content); + + const localize = (value) => + t(`solutions-wallets-explorer.wallet-filters.${value}`); + + // Map of group names to their corresponding icons + const groupIcons = { + "Wallet Type": ( + {t("solutions-wallets-explorer.filter-icons.wallet-type-icon-alt")} + ), + "Financial Transactions": ( + {t( + ), + "Asset Management": ( + {t( + ), + "Security and Recovery": ( + {t( + ), + "Solana Tooling": ( + {t( + ), + }; + + const MobileCollapsibleContentRow = ({ + groupName, + values, + maxValuesShown, + }) => { + if (values.length === 0) return null; + + if (values.length === maxValuesShown) { + return ( +
    + {groupIcons[groupName]} +

    + {values.map((value, index) => ( + + {localize(value)} + {index === values.length - 1 ? null : } + + ))} +

    +
    + ); + } + + if (values.length > maxValuesShown - 1) { + return ( +
    + {groupIcons[groupName]} +

    + {values.slice(0, maxValuesShown).map((value, index) => ( + + {localize(value)} + + + ))} + +{values.length - maxValuesShown} +

    +
    + ); + } + + return ( +
    + {groupIcons[groupName]} +

    {localize(values[0])}

    +
    + ); + }; + + const MobileTags = () => ( +
    + {tags.length > 0 && ( +
    + + {tags.length > 1 && ( + +{tags.length - 1} + )} +
    + )} +
    + ); + + const DesktopTags = () => ( +
    + {tags.length > 0 && ( +
    + {tags.map((tag, index) => ( + + ))} +
    + )} +
    + ); + + return ( + +
    + + + + +
    +
    + + {name} + +
    + +
    + + {name} + + + + + + +
    + + + + + +
    + +
    + {groupedContent.map((group, index) => { + if (group.values.length === 0) return null; + return ( +
    + {groupIcons[group.groupName]} +

    + {group.values.map((value, index) => ( + + {localize(value)} + {index === group.values.length - 1 ? null : } + + ))} +

    +
    + ); + })} +
    +
    + + + {t("solutions-wallets-explorer.visit-website")} + + +
    +
    +
    +
    + ); +}; + +export default WalletCard; diff --git a/src/components/solutions/wallets-explorer/WalletCard.module.scss b/src/components/solutions/wallets-explorer/WalletCard.module.scss new file mode 100644 index 00000000..eccf50f1 --- /dev/null +++ b/src/components/solutions/wallets-explorer/WalletCard.module.scss @@ -0,0 +1,176 @@ +@import "~@/scss/solutions/_variables.scss"; + +.WalletCard { + border-bottom: 1px solid #322f43; + padding: 20px 0; + position: relative; + + @include breakpoint(lg) { + padding: 40px 0; + } +} + +.WalletCardContainer { + display: flex; + gap: 15px; +} + +.CollapsibleTrigger { + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +.WalletCardImage { + width: 64px; + min-width: 64px; + overflow: hidden; + border-radius: 8px; + + img { + width: 100%; + } +} + +.WalletCardContent { + display: flex; + flex-direction: column; + gap: 12px; +} + +.WalletCardTitle { + font-size: 18px; + font-weight: 700; + line-height: 1.1; + margin-bottom: 0; + color: #fff; + transition: 0.2s all ease; + + @include breakpoint(lg) { + font-size: 24px; + } +} + +.TagsWrapper { + display: flex; + gap: 10px; +} + +.Tags { + display: flex; + gap: 12px; + align-items: center; + font-family: var(--font-mono); +} + +.Tag { + font-size: 13px; + font-weight: 400; + line-height: 1.4; + letter-spacing: 0.02em; + text-transform: uppercase; + color: #a2a1b2; + padding: 8px 12px; + border-radius: 40px; + background: #322f43; + + @include breakpoint(lg) { + line-height: 1.15; + } +} + +.ExtraTagsCount { + font-size: 13px; + font-weight: 400; + line-height: 1.4; + letter-spacing: 0.02em; +} + +.WalletCardLink { + color: #fff; + font-family: "ABC Mono"; + font-size: 13px; + font-weight: 400; + line-height: 1.4; + letter-spacing: 0.26px; + text-transform: uppercase; + margin-top: 12px; + transition: color 0.2s ease; + display: flex; + align-items: center; + gap: 4px; + + @media (min-width: 768px) { + font-size: 15px; + line-height: 1.15; + letter-spacing: 0.3px; + margin-top: 28px; + } +} + +.MobileContent { + @include breakpoint(lg) { + display: none; + } +} + +.DesktopContent { + display: none; + @include breakpoint(lg) { + display: block; + } +} + +.GroupedContent { + display: flex; + align-items: center; + gap: 16px; + + & + .GroupedContent { + margin-top: 12px; + } + + .GroupedContentText { + margin-bottom: 0; + color: #a2a1b2; + font-size: 13px; + font-weight: 700; + line-height: 1.32; + + &, + .ValueGroup { + display: flex; + align-items: center; + gap: 8px; + } + + @include breakpoint(lg) { + font-size: 15px; + line-height: 1.32; + } + } + + svg { + width: 17px; + height: 17px; + } +} + +.Dot { + font-size: 6px; +} + +.MobileTags { + @include breakpoint(lg) { + display: none; + } +} + +.DesktopTags { + display: none; + + @include breakpoint(lg) { + display: block; + } +} diff --git a/src/components/solutions/wallets-explorer/WalletFilters.jsx b/src/components/solutions/wallets-explorer/WalletFilters.jsx new file mode 100644 index 00000000..3e8da024 --- /dev/null +++ b/src/components/solutions/wallets-explorer/WalletFilters.jsx @@ -0,0 +1,115 @@ +import { useState } from "react"; + +import { MobileFilters, DesktopFilters } from "./Filters"; + +import Wallets from "./Wallets"; + +import styles from "./WalletFilters.module.scss"; + +const WalletFilters = ({ + filterData, + currentFilters, + setFilters, + updateWallets, + walletData, +}) => { + /** + * Returns all filter data with an extra key named `checked` set to false as the initial state of a filter + * @returns Object + */ + const setFilterInitialState = () => { + return filterData.map((filter) => ({ ...filter, checked: false })); + }; + + const [filterState, setFilterState] = useState(setFilterInitialState()); + + // Accepts the index of the filter and changes its checked state, + // then updates filterState to use the updated state + const toggleFilterActiveState = (index) => { + console.log({ index }); + const filters = [...filterState]; + filters[index].checked = !filters[index].checked; + + setFilterState(filters); + + const activeFilters = { ...currentFilters }; + const filterKey = filters[index].filterKey; + + // Add the filter as true to the filters state or remove the filter if its been unchecked + if (filters[index].checked) { + if (!(filterKey in activeFilters)) { + activeFilters[filterKey] = true; + } + } else { + if (filterKey in activeFilters) { + delete activeFilters[filterKey]; + } + } + + const showAllButton = document.querySelector( + 'button[data-role="show-all"]', + ); + + setFilters(activeFilters); + + // Handle case where a user checks a filter and then unchecks that same filter. If we have no filters active, + // show all wallets and set the "All wallets" button as active. If we have filters at least one filter active, + // remove active state from "All wallets" and show filtered wallets + if (Object.keys(activeFilters).length > 0) { + if ( + showAllButton && + showAllButton.classList.contains(`${styles["wallet-filter--active"]}`) + ) { + showAllButton.classList.remove(`${styles["wallet-filter--active"]}`); + } + + // Filters are active - show filtered results + updateWallets(activeFilters); + } else { + if ( + showAllButton && + !showAllButton.classList.contains(`${styles["wallet-filter--active"]}`) + ) { + showAllButton.classList.add(`${styles["wallet-filter--active"]}`); + } + + // No filters are active - show all + updateWallets({}); + } + }; + + const resetWalletsAndFilters = () => { + setFilterState(setFilterInitialState()); // Reset all filters to unchecked + setFilters({}); // Remove all current filters + updateWallets({}); // Pass empty filters object to show all wallets + + const showAllButton = document.querySelector( + 'button[data-role="show-all"]', + ); + + if ( + showAllButton && + !showAllButton.classList.contains(`${styles["wallet-filter--active"]}`) + ) { + showAllButton.classList.add(`${styles["wallet-filter--active"]}`); + } + }; + + return ( +
    + + + +
    + ); +}; + +export default WalletFilters; diff --git a/src/components/solutions/wallets-explorer/WalletFilters.module.scss b/src/components/solutions/wallets-explorer/WalletFilters.module.scss new file mode 100644 index 00000000..7bd6538b --- /dev/null +++ b/src/components/solutions/wallets-explorer/WalletFilters.module.scss @@ -0,0 +1,65 @@ +@import "~@/scss/solutions/_variables.scss"; + +.WalletFilters { + padding: 0 24px 64px; + max-width: 1068px; + margin: 0 auto; + + @include breakpoint(lg) { + display: grid; + grid-template-columns: 330px 1fr; + gap: 40px; + } +} + +.wallet-filter-section { + display: flex; + flex-flow: row wrap; + justify-content: flex-start; + align-items: center; + gap: 5px; + max-width: 1280px; + margin: 0 auto; + padding: 0 20px; + position: relative; + z-index: 10; +} + +.wallet-filter { + display: inline-block; + background-color: #504d61; + color: #fff; + transition: all 0.3s ease; + border-radius: 30px; + padding: 7px 12px; + font-size: 12px; + text-transform: uppercase; + min-width: 50px; + text-align: center; + font-family: "Diatype", monotype; + line-height: 13px; + border: 1px solid #504d61; + + &:hover { + background-color: #1d1a23; + cursor: pointer; + } +} + +.wallet-filter--active { + background-color: #1d1a23; + color: #fff; +} + +.wallet-filter + .wallet-filter-input:checked { + background-color: #1d1a23; +} + +.wallet-filter.active { + background-color: #1d1a23; + color: #fff; +} + +.wallet-filter-input { + appearance: none; +} diff --git a/src/components/solutions/wallets-explorer/Wallets.jsx b/src/components/solutions/wallets-explorer/Wallets.jsx new file mode 100644 index 00000000..63c89bbd --- /dev/null +++ b/src/components/solutions/wallets-explorer/Wallets.jsx @@ -0,0 +1,61 @@ +import { useTranslation } from "next-i18next"; + +import { walletData } from "../../../data/wallets/wallet-data"; + +import WalletCard from "./WalletCard"; +import { MotionSlideIn } from "@/components/shared/Motions"; + +import styles from "./Wallets.module.scss"; + +const Wallets = ({ filteredWalletData }) => { + const { t } = useTranslation(); + + return ( + <> +
    + +

    + {t("solutions-wallets-explorer.showing")}{" "} + + {filteredWalletData.length} / {walletData.length} + {" "} + {t("solutions-wallets-explorer.wallets")} +

    +
    + +
    + {filteredWalletData.length ? ( + filteredWalletData.map((wallet, key) => { + const { ...walletDataContent } = wallet; + + return ( + + + + ); + }) + ) : ( +
    +

    + {t("solutions-wallets-explorer.no-results")} +

    +
    + )} +
    +
    + + ); +}; + +export default Wallets; diff --git a/src/components/solutions/wallets-explorer/Wallets.module.scss b/src/components/solutions/wallets-explorer/Wallets.module.scss new file mode 100644 index 00000000..3f351fb4 --- /dev/null +++ b/src/components/solutions/wallets-explorer/Wallets.module.scss @@ -0,0 +1,13 @@ +.WalletsCount { + font-size: 15px; + font-weight: 700; + line-height: 1.32; + text-align: left; + padding: 0 0 20px; + color: #6c6a81; + margin-bottom: 0; + + .Count { + color: white; + } +} diff --git a/src/components/solutions/wallets-explorer/WalletsLayout.jsx b/src/components/solutions/wallets-explorer/WalletsLayout.jsx new file mode 100644 index 00000000..f5564537 --- /dev/null +++ b/src/components/solutions/wallets-explorer/WalletsLayout.jsx @@ -0,0 +1,72 @@ +import { useState } from "react"; +import { useTranslation } from "next-i18next"; +import Lottie from "react-lottie"; + +import { walletData } from "../../../data/wallets/wallet-data"; +import { walletFiltersData } from "../../../data/wallets/wallet-filters"; + +import WalletFilters from "./WalletFilters"; +import { OpacityInText } from "@/components/shared/Text"; + +import styles from "./WalletsLayout.module.scss"; + +import * as walletHeroLottie from "../../../../assets/wallets/wallet-finder.json"; + +const WalletsLayout = () => { + const { t } = useTranslation(); + + const [filters, setFilters] = useState({}); + const [wallets, setWallets] = useState(walletData); + + const updateWalletsBasedOnFilters = (filters) => { + if (Object.keys(filters).length !== 0) { + // Only return wallets that match every key/value pair inside the filters object + const updatedWallets = walletData.filter((obj) => + Object.keys(filters).every((key) => obj[key] === filters[key]), + ); + + setWallets(updatedWallets); + } else { + setWallets(walletData); + } + }; + + return ( +
    +
    +
    +
    + + {t("solutions-wallets-explorer.hero.title")} + + + + {t("solutions-wallets-explorer.hero.text")} + +
    + +
    + +
    +
    +
    + + +
    + ); +}; + +export default WalletsLayout; diff --git a/src/components/solutions/wallets-explorer/WalletsLayout.module.scss b/src/components/solutions/wallets-explorer/WalletsLayout.module.scss new file mode 100644 index 00000000..0a8d7167 --- /dev/null +++ b/src/components/solutions/wallets-explorer/WalletsLayout.module.scss @@ -0,0 +1,92 @@ +@import "~@/scss/solutions/_variables.scss"; + +.WalletLayout { + background: #0f0a16; +} + +.Hero { + padding: 64px 0; + position: relative; + background-image: url("/src/img/solana-wallets/hero-mb.svg"); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + + @media (min-width: 640px) { + padding: 128px 0; + background-image: url("/src/img/solana-wallets/hero-dt.svg"); + background-position: right center; + } + + .HeroContainer { + padding: 0 24px; + display: grid; + grid-template-columns: 1fr; + grid-gap: 40px; + align-items: center; + justify-content: center; + max-width: 1080px; + margin: 0 auto; + + @include breakpoint(md) { + grid-template-columns: 0.8fr 1fr; + } + + @include breakpoint(lg) { + padding: 40px; + } + } + + h2 { + font-size: 36px; + font-weight: 700; + line-height: 1.14; + letter-spacing: -0.01em; + text-align: center; + margin: 0; + } + + p { + font-size: 20px; + font-weight: 700; + line-height: 1.2; + text-align: center; + color: #a2a1b2; + margin: 24px 0 0; + max-width: 100%; + } + + @include breakpoint(md) { + h2, + p { + text-align: left; + } + h2 { + font-size: 48px; + } + } + + @include breakpoint(lg) { + h2 { + font-size: 64px; + font-weight: 700; + line-height: 1.04; + letter-spacing: -0.02em; + text-align: left; + } + p { + font-size: 24px; + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.02em; + text-align: left; + } + } +} + +.LottieContainer { + height: 407px; + [role="button"] { + cursor: initial; + } +} diff --git a/src/components/solutions/wallets/WalletTitle.jsx b/src/components/solutions/wallets/WalletTitle.jsx new file mode 100644 index 00000000..3c3fd1f0 --- /dev/null +++ b/src/components/solutions/wallets/WalletTitle.jsx @@ -0,0 +1,35 @@ +import classNames from "classnames"; +import { Trans } from "next-i18next"; + +import { motion } from "framer-motion"; +import { useInView } from "react-intersection-observer"; + +import { animations, easeFunctions, durations } from "@/constants/animations"; + +const transition = { + duration: durations.slower, + ease: easeFunctions.easeInQuart, +}; + +const WalletTitle = ({ styleName, text }) => { + const { ref, inView } = useInView({ + triggerOnce: true, + threshold: 0.25, + }); + + return ( + +

    + +

    +
    + ); +}; + +export default WalletTitle; diff --git a/src/components/solutions/wallets/WalletsDetails.jsx b/src/components/solutions/wallets/WalletsDetails.jsx new file mode 100644 index 00000000..8b28458b --- /dev/null +++ b/src/components/solutions/wallets/WalletsDetails.jsx @@ -0,0 +1,65 @@ +import classNames from "classnames"; +import DetailsSection, { Detail } from "@/components/solutions/DetailsSection"; +import { useTranslation, Trans } from "next-i18next"; +import { useInView } from "react-intersection-observer"; +import { MotionSlideIn } from "@/components/shared/Motions"; + +const WalletsDetails = ({ styles }) => { + const { t } = useTranslation(); + const { ref: inViewRef, inView } = useInView({ + triggerOnce: true, + threshold: 0.5, + }); + + return ( +
    + +

    + +

    +
    + + + + + + + + + + + +
    + ); +}; + +export default WalletsDetails; diff --git a/src/components/solutions/wallets/WalletsExploreSolutions.jsx b/src/components/solutions/wallets/WalletsExploreSolutions.jsx new file mode 100644 index 00000000..444df853 --- /dev/null +++ b/src/components/solutions/wallets/WalletsExploreSolutions.jsx @@ -0,0 +1,62 @@ +import classNames from "classnames"; +import { useTranslation, Trans } from "next-i18next"; +import Button from "@/components/solutions/Button"; +import { MotionSlideIn } from "@/components/shared/Motions"; +import Text, { AnimatedText, GradientText } from "@/components/shared/Text"; + +const WalletsExploreSolutions = ({ styles }) => { + const { t } = useTranslation(); + + return ( +
    + + + ), + }} + /> + + + + {t("solutions-wallets.explore.subtitle")} + + +
    + +

    {t("solutions-wallets.explore.cards.new.title")}

    +

    {t("solutions-wallets.explore.cards.new.subtitle")}

    +
    + + + + {t("solutions-wallets.explore.cards.embed.title")} + + + {t("solutions-wallets.explore.cards.embed.subtitle")} + + +
    + + +
    + ); +}; + +export default WalletsExploreSolutions; diff --git a/src/components/solutions/wallets/WalletsHero.jsx b/src/components/solutions/wallets/WalletsHero.jsx new file mode 100644 index 00000000..0379e75e --- /dev/null +++ b/src/components/solutions/wallets/WalletsHero.jsx @@ -0,0 +1,103 @@ +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "next-i18next"; +import classNames from "classnames"; + +import useReducedMotion from "@/hooks/useReducedMotion"; + +import Button from "@/components/solutions/Button"; +import { OpacityInText } from "@/components/shared/Text"; + +import styles from "./WalletsHero.module.scss"; + +import { MotionSlideIn } from "@/components/shared/Motions"; + +const WalletsHero = () => { + const { t } = useTranslation(); + + const [active, setActive] = useState(false); + const videoRef = useRef(null); + const [prefersReducedMotion] = useReducedMotion(); + + useEffect(() => { + setTimeout(() => setActive(true), 500); + }, []); + + useEffect(() => { + prefersReducedMotion && videoRef.current.pause(); + }, [prefersReducedMotion]); + + return ( +
    +
    + + {t("solutions-wallets.hero.kicker")} + + + + {t("solutions-wallets.hero.title")} + + + + {t("solutions-wallets.hero.subtitle")} + + + +
    + +
    + +
    +
    + ); +}; + +export default WalletsHero; diff --git a/src/components/solutions/wallets/WalletsHero.module.scss b/src/components/solutions/wallets/WalletsHero.module.scss new file mode 100644 index 00000000..56bceb42 --- /dev/null +++ b/src/components/solutions/wallets/WalletsHero.module.scss @@ -0,0 +1,147 @@ +@import "~@/scss/solutions/_variables.scss"; + +.WalletsHero { + padding: 64px 24px; + display: flex; + flex-direction: column; + gap: 40px; + + .ContentWrapper { + display: flex; + flex-direction: column; + gap: 24px; + + p, + h1 { + margin-top: 0; + margin-bottom: 0; + } + } + + .Kicker { + font-size: 16px; + font-weight: 800; + line-height: 1.25; + letter-spacing: -0.02em; + text-align: center; + color: var(--blue); + } + + .Title { + font-size: 36px; + font-weight: 700; + line-height: 1.14; + letter-spacing: -0.01em; + text-align: center; + } + + .Subtitle { + font-size: 20px; + font-weight: 700; + line-height: 1.2; + text-align: center; + color: var(--grey-250); + } + + .Buttons { + display: flex; + flex-direction: column; + gap: 16px; + width: max-content; + margin: 0 auto; + } +} + +.VideoWrapper { + display: flex; + align-items: center; + opacity: 0; + transition: opacity $duration-standard $easeInQuart; + transition-delay: 300ms; + + &.Active { + opacity: 1; + } + + video { + width: 100%; + height: 100%; + object-fit: contain; + max-width: 600px; + margin: 0 auto; + } +} + +@include breakpoint(md) { + .WalletsHero { + padding: 80px 24px; + + .ContentWrapper { + @include max-width(650px); + } + + .Title { + font-size: 64px; + } + + .Subtitle { + font-size: 24px; + } + + .Buttons { + flex-direction: row; + } + } +} + +@include breakpoint(xl) { + .WalletsHero { + padding: 92px 24px; + flex-direction: row; + justify-content: space-between; + gap: 128px; + + .ContentWrapper { + max-width: 520px; + justify-content: center; + flex: 1; + + * { + text-align: left; + } + } + + .VideoWrapper { + flex: 2; + height: 670px; + max-width: 500px; + } + + .Kicker { + margin-left: 0; + font-size: 22px; + } + + .Title { + line-height: 1; + letter-spacing: -0.02em; + max-width: 422px; + } + + .Subtitle { + font-size: 28px; + line-height: 1.15; + letter-spacing: -0.02em; + max-width: 100%; + } + + .Buttons { + flex-direction: row; + margin-left: 0; + + a { + padding: 12px; + } + } + } +}