chore: heygermany customer project
This commit is contained in:
222
apps/website/components/common/ExpandableSteps.tsx
Normal file
222
apps/website/components/common/ExpandableSteps.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import { useState, useRef, useEffect, ReactNode } from 'react'
|
||||
import {
|
||||
Container,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Box,
|
||||
rem,
|
||||
Collapse,
|
||||
useMantineTheme,
|
||||
Flex,
|
||||
} from '@pikku/mantine/core'
|
||||
import { asI18n } from '@pikku/react'
|
||||
|
||||
export interface ExpandableStep {
|
||||
title: string
|
||||
icon: ReactNode
|
||||
content: () => ReactNode
|
||||
}
|
||||
|
||||
interface ExpandableStepsProps {
|
||||
steps: ExpandableStep[]
|
||||
defaultExpanded?: number[]
|
||||
singleExpand?: boolean
|
||||
containerSize?: string
|
||||
showTitle?: boolean
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function ExpandableSteps({
|
||||
steps,
|
||||
defaultExpanded = [0],
|
||||
singleExpand = true,
|
||||
containerSize = 'xl',
|
||||
showTitle = false,
|
||||
title,
|
||||
}: ExpandableStepsProps) {
|
||||
const theme = useMantineTheme()
|
||||
const [expandedSteps, setExpandedSteps] = useState(() => new Set(defaultExpanded))
|
||||
const contentRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const [heights, setHeights] = useState<number[]>([])
|
||||
|
||||
const CIRCLE_SIZE = 38
|
||||
const GAP = 8
|
||||
|
||||
useEffect(() => {
|
||||
const observers: ResizeObserver[] = []
|
||||
|
||||
contentRefs.current.forEach((el, i) => {
|
||||
if (!el) return
|
||||
const observer = new ResizeObserver(() => {
|
||||
setHeights((prev) => {
|
||||
const copy = [...prev]
|
||||
copy[i] = el.offsetHeight
|
||||
return copy
|
||||
})
|
||||
})
|
||||
observer.observe(el)
|
||||
observers.push(observer)
|
||||
})
|
||||
|
||||
return () => {
|
||||
observers.forEach((observer) => observer.disconnect())
|
||||
}
|
||||
}, [])
|
||||
|
||||
const calculateLineHeight = (index: number): number => {
|
||||
if (index === steps.length - 1) return 0
|
||||
const contentHeight = heights[index] ?? 0
|
||||
return contentHeight + GAP * 2
|
||||
}
|
||||
|
||||
const toggleStep = (index: number) => {
|
||||
if (singleExpand) {
|
||||
setExpandedSteps(() => new Set([index]))
|
||||
} else {
|
||||
setExpandedSteps((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(index)) {
|
||||
newSet.delete(index)
|
||||
} else {
|
||||
newSet.add(index)
|
||||
}
|
||||
return newSet
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size='xl' py="xl" mt="xl">
|
||||
<Stack
|
||||
gap="lg"
|
||||
mt="xl"
|
||||
w="100%"
|
||||
maw={{ base: rem(300), md: rem(800) }}
|
||||
mx="auto"
|
||||
>
|
||||
{showTitle && title && (
|
||||
<Text fz="xxll" fw={700} ta='center' mb='xl'>
|
||||
{asI18n(title)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{steps.map((step, index) => {
|
||||
const isActive = expandedSteps.has(index)
|
||||
const lineHeight =
|
||||
isActive && index !== steps.length - 1
|
||||
? calculateLineHeight(index)
|
||||
: 0
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={step.title}
|
||||
pos="relative"
|
||||
display="flex"
|
||||
pl={{ base: 0, md: 30 }}
|
||||
mb={8}
|
||||
mih={40}
|
||||
c={isActive ? theme.colors.primary[6] : theme.colors.gray[9]}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Box
|
||||
w={CIRCLE_SIZE}
|
||||
pos="relative"
|
||||
display="flex"
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
w={CIRCLE_SIZE}
|
||||
h={CIRCLE_SIZE}
|
||||
bg="white"
|
||||
c={
|
||||
isActive ? theme.colors.primary[6] : theme.colors.gray[9]
|
||||
}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
border: `2px solid ${theme.colors.gray[4]}`,
|
||||
fontSize: rem(16),
|
||||
fontWeight: 700,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transform: `translateY(${rem(4)})`,
|
||||
zIndex: 2,
|
||||
}}
|
||||
onClick={() => toggleStep(index)}
|
||||
>
|
||||
{index + 1}
|
||||
</Box>
|
||||
|
||||
{lineHeight > 0 && (
|
||||
<Box
|
||||
w={2}
|
||||
h={lineHeight}
|
||||
bg={`${theme.colors.gray[4]}`}
|
||||
mt={GAP}
|
||||
mb={GAP}
|
||||
style={{
|
||||
transition:
|
||||
'height 300ms cubic-bezier(.4,2,.6,1), background-color 250ms ease',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box flex={1} me={10} onClick={() => toggleStep(index)}>
|
||||
<Flex
|
||||
align="flex-start"
|
||||
gap="xs"
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
mb={6}
|
||||
>
|
||||
<ThemeIcon
|
||||
size={44}
|
||||
variant="transparent"
|
||||
color={isActive ? 'primary' : `${theme.colors.gray[8]}`}
|
||||
radius="xl"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{step.icon}
|
||||
</ThemeIcon>
|
||||
<Text
|
||||
fw={700}
|
||||
fz={25}
|
||||
style={{
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
ta={'left'}
|
||||
c={isActive ? 'primary' : `${theme.colors.gray[8]}`}
|
||||
>
|
||||
{asI18n(step.title)}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Collapse in={isActive} transitionDuration={250}>
|
||||
<Container
|
||||
ref={(el) => {
|
||||
contentRefs.current[index] = el
|
||||
}}
|
||||
id={`step-${index}-content`}
|
||||
p={0}
|
||||
mb="md"
|
||||
>
|
||||
{step.content()}
|
||||
</Container>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user