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 }
+}