CardButton.tsx176 lines · main
1import { ChevronRight, Loader } from 'lucide-react'
2import Link from 'next/link'
3import React, { cloneElement, PropsWithChildren } from 'react'
4import { cn } from 'ui'
5
6interface CardButtonProps {
7 title?: string | React.ReactNode
8 description?: string
9 footer?: React.ReactNode
10 url?: string
11 linkHref?: string
12 imgUrl?: string
13 imgAlt?: string
14 onClick?: () => void
15 icon?: React.ReactNode
16 loading?: boolean
17 className?: string
18 fixedHeight?: boolean
19 hideChevron?: boolean
20 titleClass?: string
21 containerElement?: React.ReactNode
22}
23
24// Define separate interfaces for each type of container
25interface LinkContainerProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'title'> {
26 href: string
27}
28
29interface UrlContainerProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'title'> {
30 href: string
31}
32
33interface NonLinkContainerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {}
34
35interface ButtonContainerProps extends Omit<
36 React.ButtonHTMLAttributes<HTMLButtonElement>,
37 'title'
38> {}
39
40// Union of all container props
41type ContainerProps =
42 | LinkContainerProps
43 | UrlContainerProps
44 | NonLinkContainerProps
45 | ButtonContainerProps
46
47const CardButton = ({
48 title,
49 description,
50 children,
51 footer,
52 url = '',
53 linkHref = '',
54 imgUrl,
55 imgAlt,
56 icon,
57 className,
58 loading = false,
59 fixedHeight = true,
60 hideChevron = false,
61 titleClass = '',
62 containerElement,
63 ...props
64}: PropsWithChildren<CardButtonProps & ContainerProps>) => {
65 const isLink = url || linkHref || props.onClick
66
67 let Container: React.ElementType
68 let containerProps: ContainerProps = {}
69
70 const ContainerComponentOverride =
71 containerElement && React.isValidElement(containerElement)
72 ? (props: any) => cloneElement<any>(containerElement, { ...props })
73 : undefined
74
75 if (props.onClick) {
76 Container = ContainerComponentOverride ?? 'button'
77 containerProps = props
78 } else if (linkHref) {
79 Container = ContainerComponentOverride ?? Link
80 containerProps = {
81 href: linkHref,
82 ...props,
83 }
84 } else if (url) {
85 Container = ContainerComponentOverride ?? 'a'
86 containerProps = {
87 href: url,
88 ...props,
89 }
90 } else {
91 Container = ContainerComponentOverride ?? 'div'
92 containerProps = props
93 }
94
95 let containerClasses = [
96 'group relative text-left',
97 'bg-surface-100',
98 'border border-surface',
99 'rounded-md p-5 flex flex-row',
100 'transition ease-in-out duration-150',
101 ]
102
103 if (isLink) {
104 containerClasses = [
105 ...containerClasses,
106 'cursor-pointer',
107 'hover:bg-surface-200',
108 'hover:border-control',
109 ]
110 }
111
112 if (fixedHeight) {
113 containerClasses = [...containerClasses, 'min-h-32 md:min-h-44']
114 }
115
116 const ImageContainer = ({ children }: { children: React.ReactNode }) => {
117 return <div className="mr-4 flex flex-col">{children}</div>
118 }
119
120 const contents = (
121 <>
122 {imgUrl && (
123 <ImageContainer>
124 <img
125 className="
126 transition-all
127 group-hover:scale-110
128 "
129 src={`${imgUrl}`}
130 alt={`${imgAlt}`}
131 width="26"
132 />
133 </ImageContainer>
134 )}
135 {icon && <ImageContainer>{icon}</ImageContainer>}
136 <div className="flex h-full w-full flex-col space-y-2">
137 {typeof title === 'string' ? (
138 <h5 className={`text-foreground pr-5 ${titleClass}`}>{title}</h5>
139 ) : (
140 title
141 )}
142 {(children || description) && (
143 <div className="flex w-full flex-1 flex-col">
144 <p className="text-sm text-foreground-light">{description}</p>
145 <div className="w-full">{children && children}</div>
146 </div>
147 )}
148 {footer && <div className="w-full mt-auto!">{footer}</div>}
149 </div>
150 {isLink && (
151 <div
152 className="
153 absolute
154 right-4
155 top-4
156 text-foreground-lighter
157 transition-all
158 duration-200
159 group-hover:right-3
160 group-hover:text-foreground
161 "
162 >
163 {loading ? <Loader className="animate-spin" /> : !hideChevron ? <ChevronRight /> : <></>}
164 </div>
165 )}
166 </>
167 )
168
169 return (
170 <Container {...containerProps} className={cn(containerClasses, className)}>
171 {contents}
172 </Container>
173 )
174}
175
176export default CardButton