diff --git a/.eslintrc.js b/.eslintrc.js index 668c47a2..7162f468 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,7 @@ module.exports = { }, rules: { 'prettier/prettier': 'error', + 'react/no-unknown-property': ['error', { ignore: ['css'] }], '@typescript-eslint/consistent-type-exports': 'error', '@typescript-eslint/consistent-type-imports': [ 'error', diff --git a/pages/index.tsx b/pages/index.tsx index 12ed8ae5..09c2a609 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -16,6 +16,7 @@ import { FooterVariant } from '@src/components/FooterFull' import { GradientBG } from '@src/components/layout/GradientBG' import { HeaderPad } from '@src/components/layout/HeaderPad' import ArticleSection from '@src/components/page-sections/articleSection' +import { ImpactCardSection } from '@src/components/page-sections/ImpactCardSection' import { QuoteSection } from '@src/components/page-sections/QuoteSection' import { HomePageHero } from '@src/components/PageHeros' import { CenteredSectionHead } from '@src/components/SectionHeads' @@ -306,10 +307,20 @@ export default function Index({ - +
+ + + + +
diff --git a/src/components/SingleAccordion.tsx b/src/components/SingleAccordion.tsx index 5bb4005e..c7ae9c6a 100644 --- a/src/components/SingleAccordion.tsx +++ b/src/components/SingleAccordion.tsx @@ -142,6 +142,7 @@ function AccordionContentUnstyled({ }) return ( + // @ts-ignore, see https://github.com/pmndrs/react-spring/issues/1515 + Our impact + + + + + + + + ) +} + +function ImpactCard({ + metric, + subtitle, + tooltipText, + embellishment, + borderGradientDir = 'to right', +}: { + metric: string + subtitle: string + tooltipText?: string + embellishment?: 'top-left' | 'bottom-right' + borderGradientDir?: 'to right' | 'to left' +}) { + const theme = useTheme() + const cardRef = useRef(null) + const { relativePosition } = useMousePosition(cardRef) + + const backgroundStyle = { + '--x': `${relativePosition.x}px`, + '--y': `${relativePosition.y}px`, + } as CSSProperties + + return ( + '; + inherits: false; + initial-value: 0.3; + } + `} + $borderGradientDir={borderGradientDir} + > + {embellishment && } + + {tooltipText && ( + + + + )} + {metric} + {subtitle} + + + ) +} + +const ImpactCardsWrapperSC = styled.div(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', + gap: theme.spacing.xxlarge, + paddingBottom: theme.spacing.xxxxlarge, + [`@media (min-width: ${theme.breakpoints.desktopSmall}px)`]: { + gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', + }, +})) + +const ImpactCardSC = styled.div<{ + $borderGradientDir?: 'to right' | 'to left' +}>(({ theme, $borderGradientDir }) => ({ + position: 'relative', + borderRadius: theme.borderRadiuses.large, + overflow: 'hidden', + transition: 'filter 0.3s ease', + // first value is circular glow that follows cursor, second is actual background + background: `radial-gradient(400px circle at var(--x) var(--y),rgba(255, 255, 255, 0.06), transparent), + linear-gradient(96deg, rgba(42, 46, 55, 0.48) -95.57%, rgba(42, 46, 55, 0.16) 113.54%)`, + // trick to make a border with a gradient effect + '::before': { + transition: '--gradient-opacity 0.3s ease', + content: '""', + position: 'absolute', + inset: 0, + borderRadius: theme.borderRadiuses.large, + border: '1px solid transparent', + background: `linear-gradient(${$borderGradientDir}, #E9EBEC, rgba(233, 235, 236, var(--gradient-opacity))) border-box`, + mask: 'linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0)', + maskComposite: 'exclude', + }, + ':hover': { + '::before': { + '--gradient-opacity': 1, + }, + filter: 'brightness(1.1)', + }, +})) + +const ImpactCardContentSC = styled.div(({ theme }) => ({ + position: 'relative', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: theme.spacing.medium, + borderRadius: theme.borderRadiuses.large, + padding: theme.spacing.xxlarge, +})) + +const ImpactCardInfoIconSC = styled(InfoOutlineIcon)(({ theme }) => ({ + position: 'absolute', + cursor: 'pointer', + top: theme.spacing.medium, + right: theme.spacing.medium, +})) + +const ImpactCardMetricSC = styled.h3(({ theme }) => ({ + ...theme.partials.marketingText.hero1, + lineHeight: '100%', +})) + +const ImpactCardSubtitleSC = styled.p(({ theme }) => ({ + color: theme.colors['text-light'], + fontFamily: 'Inter', + fontSize: '28px', + fontStyle: 'normal', + fontWeight: 400, + lineHeight: '150%', +})) + +const EmblishmentSC = styled.div<{ $position: 'top-left' | 'bottom-right' }>( + ({ $position }) => { + const size = 300 + const strokeWidth = 1 + const gradientBorderSVG = encodeURIComponent(` + + + + + + + + + + + + `) + + return { + position: 'absolute', + top: $position === 'top-left' ? -size / 2 : 'auto', + left: $position === 'top-left' ? -size / 2 : 'auto', + right: $position === 'bottom-right' ? -size / 2 : 'auto', + bottom: $position === 'bottom-right' ? -size / 2 : 'auto', + width: `${size}px`, + height: `${size}px`, + backgroundImage: `url("data:image/svg+xml,${gradientBorderSVG}")`, + } + } +) diff --git a/src/components/page-sections/QuoteSection.tsx b/src/components/page-sections/QuoteSection.tsx index 7dfaff68..f7dc0a28 100644 --- a/src/components/page-sections/QuoteSection.tsx +++ b/src/components/page-sections/QuoteSection.tsx @@ -2,7 +2,6 @@ import styled, { useTheme } from 'styled-components' import { type QuoteFragment } from '@src/generated/graphqlDirectus' -import { StandardPageWidth } from '../layout/LayoutHelpers' import { QuotesCarousel } from '../QuoteCards' import { ResponsiveText } from '../Typography' @@ -16,33 +15,26 @@ export function QuoteSection({ const theme = useTheme() return ( - -
- - {title} - -
- - } - /> - -
+
+ + {title} + +
+ + } + /> +
- +
) } diff --git a/src/components/types/styled.d.ts b/src/components/types/styled.d.ts index 8028986e..95e3c78e 100644 --- a/src/components/types/styled.d.ts +++ b/src/components/types/styled.d.ts @@ -1,12 +1,19 @@ -// import original module declarations -import 'styled-components' - import { type styledTheme } from '@pluralsh/design-system' +import { type CSSProp } from 'styled-components' + +// Allow css prop on html elements +declare module 'react' { + interface Attributes { + css?: CSSProp | undefined + } +} + type StyledTheme = typeof styledTheme -// and extend them! +// extend original module declarations declare module 'styled-components' { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DefaultTheme extends StyledTheme {} + export declare function useTheme(): DefaultTheme } diff --git a/src/hooks/useMousePosition.tsx b/src/hooks/useMousePosition.tsx new file mode 100644 index 00000000..4813e347 --- /dev/null +++ b/src/hooks/useMousePosition.tsx @@ -0,0 +1,48 @@ +import { type RefObject, useEffect, useState } from 'react' + +type MouseCoordinates = { + x: number | null + y: number | null +} + +export function useMousePosition(): { mousePosition: MouseCoordinates } +export function useMousePosition(elementRef: RefObject): { + mousePosition: MouseCoordinates + relativePosition: MouseCoordinates +} + +export function useMousePosition(elementRef?: RefObject) { + const [mousePosition, setMousePosition] = useState({ + x: null, + y: null, + }) + + useEffect(() => { + const updateMousePosition = (ev: MouseEvent) => { + setMousePosition({ x: ev.clientX, y: ev.clientY }) + } + + window.addEventListener('mousemove', updateMousePosition) + + return () => { + window.removeEventListener('mousemove', updateMousePosition) + } + }, []) + + let relativePosition: MouseCoordinates = { x: null, y: null } + + if ( + elementRef?.current && + mousePosition.x !== null && + mousePosition.y !== null + ) { + const rect = elementRef.current.getBoundingClientRect() + + relativePosition = { + x: mousePosition.x - rect.left, + y: mousePosition.y - rect.top, + } + } + + return elementRef ? { mousePosition, relativePosition } : { mousePosition } +}