This commit is contained in:
2024-04-21 14:42:52 +02:00
parent 4b69674ede
commit 8a25f53c99
10700 changed files with 55767 additions and 14201 deletions

View File

@@ -0,0 +1,11 @@
import { createContext } from 'react'
type DrawerContextType = {
isCollapsed: boolean | undefined
isTouched: boolean
}
export const DrawerContext = createContext<DrawerContextType>({
isCollapsed: undefined,
isTouched: false,
})

View File

@@ -0,0 +1,92 @@
import { ReactElement, useContext, useEffect, useState } from 'react'
import { Link, useLocation } from 'react-router-dom'
import cx from 'classnames'
import { DrawerContext } from './drawer-context'
type DrawerItemProps = {
icon: ReactElement
href: string
primaryText: string
secondaryText: string
isActive?: boolean
}
export const DrawerItem = ({
icon,
href,
primaryText,
secondaryText,
}: DrawerItemProps) => {
const location = useLocation()
const [isActive, setIsActive] = useState<boolean>()
const { isCollapsed } = useContext(DrawerContext)
useEffect(() => {
if (
(href === '/' && location.pathname === '/') ||
(href !== '/' && location.pathname.startsWith(href))
) {
setIsActive(true)
} else {
setIsActive(false)
}
}, [location.pathname, href])
return (
<Link
to={href}
title={isCollapsed ? `${primaryText}: ${secondaryText}` : secondaryText}
className={cx('w-full')}
>
<div
className={cx(
'flex',
'flex-row',
'items-center',
'gap-1.5',
'p-1.5',
'rounded-md',
{
'bg-black': isActive,
'dark:bg-white': isActive,
},
{
'hover:bg-gray-100': !isActive,
'dark:hover:bg-gray-600': !isActive,
},
{
'hover:bg-gray-200': !isActive,
'dark:hover:bg-gray-700': !isActive,
},
)}
>
<div
className={cx(
'flex',
'items-center',
'justify-center',
'shrink-0',
'w-[21px]',
'h-[21px]',
{
'text-white': isActive,
'dark:text-gray-800': isActive,
},
)}
>
{icon}
</div>
{!isCollapsed && (
<span
className={cx({
'text-white': isActive,
'dark:text-gray-800': isActive,
})}
>
{primaryText}
</span>
)}
</div>
</Link>
)
}

View File

@@ -0,0 +1,129 @@
import { ReactNode, useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import cx from 'classnames'
import { StorageOptions } from '../../types'
import { IconChevronLeft, IconChevronRight } from '../icons'
import { DrawerContext } from './drawer-context'
type DrawerProps = {
children?: ReactNode
logo?: ReactNode
storage?: StorageOptions
}
export const Drawer = ({ children, storage, logo }: DrawerProps) => {
const [isCollapsed, setIsCollapsed] = useState<boolean | undefined>(undefined)
const [isTouched, setIsTouched] = useState(false)
const localStorageCollapsedKey = useMemo(
() =>
`${storage?.prefix || 'app'}_${
storage?.namespace || 'main'
}_drawer_collapsed`,
[storage],
)
useEffect(() => {
let collapse = false
if (typeof localStorage !== 'undefined') {
const value = localStorage.getItem(localStorageCollapsedKey)
if (value) {
collapse = JSON.parse(value)
} else {
localStorage.setItem(localStorageCollapsedKey, JSON.stringify(false))
}
}
if (collapse) {
setIsCollapsed(true)
} else {
setIsCollapsed(false)
}
}, [localStorageCollapsedKey, setIsCollapsed])
if (isCollapsed === undefined) {
return null
}
return (
<DrawerContext.Provider
value={{
isCollapsed,
isTouched,
}}
>
<div
className={cx(
'flex',
'flex-col',
'h-full',
'border-r',
'border-r-gray-200',
'dark:border-r-gray-700',
'shrink-0',
'gap-0',
)}
>
<div
className={cx('flex', 'items-center', 'justify-center', 'h-[80px]')}
>
<Link to="/">
<div className={cx('flex', 'h-[40px]')}>
<div
className={cx(
'flex',
'items-center',
'justify-center',
'w-[40px]',
'h-[40px]',
)}
>
{logo}
</div>
</div>
</Link>
</div>
<div
className={cx(
'flex',
'flex-col',
'items-center',
'gap-0.5',
'pt-0',
'pr-1.5',
'pb-1.5',
'pl-1.5',
)}
>
{children}
</div>
<div className={cx('grow')} />
<div
className={cx(
'flex',
'flex-row',
'items-center',
'gap-0',
{ 'justify-center': isCollapsed, 'justify-end': !isCollapsed },
'h-[50px]',
'w-full',
{ 'px-0': isCollapsed, 'px-1.5': !isCollapsed },
'cursor-pointer',
'hover:bg-gray-100',
'hover:dark:bg-gray-600',
'active:bg-gray-200',
'active:dark:bg-gray-700',
)}
onClick={() => {
setIsCollapsed(!isCollapsed)
setIsTouched(true)
localStorage.setItem(
localStorageCollapsedKey,
JSON.stringify(!isCollapsed),
)
}}
>
{isCollapsed ? <IconChevronRight /> : <IconChevronLeft />}
</div>
</div>
</DrawerContext.Provider>
)
}

View File

@@ -0,0 +1,3 @@
export * from './drawer'
export * from './drawer-context'
export * from './drawer-item'

View File

@@ -0,0 +1,395 @@
import { cx } from '@emotion/css'
export type IconBaseProps = {
filled?: boolean
} & React.HTMLAttributes<HTMLSpanElement>
type GetClassNameOptions = {
filled?: boolean
className?: string
}
function getClassName({ filled, className }: GetClassNameOptions) {
return cx(
'material-symbols-rounded',
{ 'material-symbols-rounded__filled': filled },
'text-[16px]',
className,
)
}
export const IconPlayArrow = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
play_arrow
</span>
)
export const IconUpload = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
upload
</span>
)
export const IconNotifications = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
notifications
</span>
)
export const IconMoreVert = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
more_vert
</span>
)
export const IconLogout = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
logout
</span>
)
export const IconChevronLeft = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
chevron_left
</span>
)
export const IconChevronRight = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
chevron_right
</span>
)
export const IconAdd = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
add
</span>
)
export const IconEdit = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
edit
</span>
)
export const IconGroup = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
group
</span>
)
export const IconDownload = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
download
</span>
)
export const IconArrowTopRight = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
arrow_top_right
</span>
)
export const IconFileCopy = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
file_copy
</span>
)
export const IconDelete = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
delete
</span>
)
export const IconSend = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
send
</span>
)
export const IconPersonAdd = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
person_add
</span>
)
export const IconCheck = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
check
</span>
)
export const IconLibraryAddCheck = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
library_add_check
</span>
)
export const IconSelectCheckBox = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
select_check_box
</span>
)
export const IconCheckBoxOutlineBlank = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
check_box_outline_blank
</span>
)
export const IconCheckCircle = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled: true, className })} {...props}>
check_circle
</span>
)
export const IconError = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled: true, className })} {...props}>
error
</span>
)
export const IconWarning = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
warning
</span>
)
export const IconWorkspaces = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
workspaces
</span>
)
export const IconFlag = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
flag
</span>
)
export const IconClose = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
close
</span>
)
export const IconSchedule = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
schedule
</span>
)
export const IconClearAll = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
clear_all
</span>
)
export const IconOpenInNew = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
open_in_new
</span>
)
export const IconInfo = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
info
</span>
)
export const IconSearch = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
search
</span>
)
export const IconRefresh = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
refresh
</span>
)
export const IconGridView = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
grid_view
</span>
)
export const IconArrowUpward = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
arrow_upward
</span>
)
export const IconArrowDownward = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
arrow_downward
</span>
)
export const IconExpandMore = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
expand_more
</span>
)
export const IconList = ({ className, filled, ...props }: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
list
</span>
)
export const IconHourglass = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
hourglass
</span>
)
export const IconKeyboardArrowLeft = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
keyboard_arrow_left
</span>
)
export const IconKeyboardArrowRight = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
keyboard_arrow_right
</span>
)
export const IconKeyboardDoubleArrowRight = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
keyboard_double_arrow_right
</span>
)
export const IconKeyboardDoubleArrowLeft = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
keyboard_double_arrow_left
</span>
)
export const IconFirstPage = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
first_page
</span>
)
export const IconLastPage = ({
className,
filled,
...props
}: IconBaseProps) => (
<span className={getClassName({ filled, className })} {...props}>
last_page
</span>
)

View File

@@ -0,0 +1,10 @@
export * from './drawer'
export * from './icons'
export * from './spinner'
export * from './section-spinner'
export * from './shell'
export * from './pagination'
export * from './page-pagination'
export * from './search-input'
export * from './switch-card'
export * from './text'

View File

@@ -0,0 +1,72 @@
import React, { ChangeEvent, useCallback } from 'react'
import { Select } from '@chakra-ui/react'
import cx from 'classnames'
import { usePageMonitor } from '../hooks/page-monitor'
import { Pagination } from './pagination'
type PagePaginationProps = {
totalPages: number
totalElements: number
page: number
size: number
steps: number[]
uiSize?: string
style?: React.CSSProperties
setPage: (page: number) => void
setSize: (size: number) => void
}
export const PagePagination = ({
totalElements,
totalPages,
page,
size,
uiSize = 'md',
steps,
style,
setPage,
setSize,
}: PagePaginationProps) => {
const { hasPageSwitcher, hasSizeSelector } = usePageMonitor({
totalElements,
totalPages,
steps,
})
const handleSizeChange = useCallback(
(event: ChangeEvent<HTMLSelectElement>) => {
setSize(parseInt(event.target.value))
setPage(1)
},
[setSize, setPage],
)
return (
<>
{!hasPageSwitcher && !hasSizeSelector ? null : (
<div
className={cx('flex', 'flex-row', 'items-center', 'gap-1.5')}
style={style}
>
{hasPageSwitcher ? (
<Pagination
uiSize={uiSize}
page={page}
totalPages={totalPages}
onPageChange={setPage}
/>
) : null}
{hasSizeSelector ? (
<Select defaultValue={size} onChange={handleSizeChange}>
{steps.map((step, index) => (
<option key={index} value={step.toString()}>
{step} items
</option>
))}
</Select>
) : null}
</div>
)}
</>
)
}

View File

@@ -0,0 +1,112 @@
import { useCallback, useMemo } from 'react'
import { ButtonGroup, Button, IconButton } from '@chakra-ui/react'
import {
IconKeyboardArrowLeft,
IconKeyboardArrowRight,
IconKeyboardDoubleArrowLeft,
IconKeyboardDoubleArrowRight,
IconFirstPage,
IconLastPage,
} from '@/lib'
type PaginationProps = {
totalPages: number
page: number
maxButtons?: number
uiSize?: string
onPageChange?: (page: number) => void
}
export const Pagination = ({
totalPages,
page,
maxButtons: maxButtonsProp = 5,
uiSize = 'md',
onPageChange,
}: PaginationProps) => {
const maxButtons = totalPages < maxButtonsProp ? totalPages : maxButtonsProp
const pages = useMemo(() => {
const end = Math.ceil(page / maxButtons) * maxButtons
const start = end - maxButtons + 1
return Array.from({ length: end - start + 1 }, (_, index) => start + index)
}, [page, maxButtons])
const firstPage = 1
const lastPage = totalPages
const fastForwardPage = pages[pages.length - 1] + 1
const rewindPage = pages[0] - maxButtons
const nextPage = page + 1
const previousPage = page - 1
const handlePageChange = useCallback(
(value: number) => {
if (value !== page) {
onPageChange?.(value)
}
},
[page, onPageChange],
)
return (
<ButtonGroup>
<IconButton
variant="outline"
size={uiSize}
isDisabled={page === 1}
icon={<IconFirstPage />}
aria-label="First"
onClick={() => handlePageChange(firstPage)}
/>
<IconButton
variant="outline"
size={uiSize}
isDisabled={rewindPage < 1}
icon={<IconKeyboardDoubleArrowLeft />}
aria-label="Rewind"
onClick={() => handlePageChange(rewindPage)}
/>
<IconButton
variant="outline"
size={uiSize}
isDisabled={page === 1}
icon={<IconKeyboardArrowLeft />}
aria-label="Previous"
onClick={() => handlePageChange(previousPage)}
/>
{pages.map((index) => (
<Button
size={uiSize}
key={index}
isDisabled={index > totalPages}
onClick={() => handlePageChange(index)}
colorScheme={index === page ? 'blue' : undefined}
>
{index}
</Button>
))}
<IconButton
variant="outline"
size={uiSize}
isDisabled={page === lastPage}
icon={<IconKeyboardArrowRight />}
aria-label="Next"
onClick={() => handlePageChange(nextPage)}
/>
<IconButton
variant="outline"
size={uiSize}
isDisabled={fastForwardPage > lastPage}
icon={<IconKeyboardDoubleArrowRight />}
aria-label="Fast Forward"
onClick={() => handlePageChange(fastForwardPage)}
/>
<IconButton
variant="outline"
size={uiSize}
isDisabled={page === lastPage}
icon={<IconLastPage />}
aria-label="Last"
onClick={() => handlePageChange(lastPage)}
/>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,93 @@
import {
useCallback,
useEffect,
useState,
ChangeEvent,
KeyboardEvent,
} from 'react'
import {
Button,
HStack,
InputGroup,
InputLeftElement,
InputRightElement,
IconButton,
Input,
} from '@chakra-ui/react'
import cx from 'classnames'
import { IconClose, IconSearch } from './icons'
type SearchInputProps = {
query?: string
onChange?: (value: string) => void
}
export const SearchInput = ({ query, onChange }: SearchInputProps) => {
const [draft, setDraft] = useState('')
const [text, setText] = useState('')
const [isFocused, setIsFocused] = useState(false)
useEffect(() => {
setDraft(query || '')
}, [query])
useEffect(() => {
onChange?.(text)
}, [text, onChange])
const handleClear = useCallback(() => {
setDraft('')
setText('')
}, [])
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setDraft(event.target.value || '')
}, [])
const handleSearch = useCallback((value: string) => {
setText(value)
}, [])
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLInputElement>, value: string) => {
if (event.key === 'Enter') {
handleSearch(value)
}
},
[handleSearch],
)
return (
<HStack>
<InputGroup>
<InputLeftElement className={cx('pointer-events-none')}>
<IconSearch className={cx('text-gray-300')} />
</InputLeftElement>
<Input
value={draft}
placeholder={draft || 'Search'}
variant="filled"
onKeyDown={(event) => handleKeyDown(event, draft)}
onChange={handleChange}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
{draft && (
<InputRightElement>
<IconButton
icon={<IconClose />}
onClick={handleClear}
size="xs"
aria-label="Clear"
/>
</InputRightElement>
)}
</InputGroup>
{draft || (isFocused && draft) ? (
<Button onClick={() => handleSearch(draft)} isDisabled={!draft}>
Search
</Button>
) : null}
</HStack>
)
}

View File

@@ -0,0 +1,19 @@
import cx from 'classnames'
import { Spinner } from './spinner'
type SectionSpinnerProps = {
width?: string
height?: string
}
const DEFAULT_WIDTH = '100%'
const DEFAULT_HEIGHT = '300px'
export const SectionSpinner = ({ width, height }: SectionSpinnerProps) => (
<div
className={cx('flex', 'items-center', 'justify-center')}
style={{ width: width || DEFAULT_WIDTH, height: height || DEFAULT_HEIGHT }}
>
<Spinner />
</div>
)

View File

@@ -0,0 +1,59 @@
import { ReactElement } from 'react'
import cx from 'classnames'
import { StorageOptions } from '../types'
import { Drawer, DrawerItem } from './drawer'
type ShellItem = {
href: string
icon: ReactElement
primaryText: string
secondaryText: string
}
type ShellProps = {
storage?: StorageOptions
logo: ReactElement
topBar: ReactElement
items: ShellItem[]
children?: ReactElement
}
export const Shell = ({
logo,
topBar,
items,
storage,
children,
}: ShellProps) => (
<div className={cx('flex', 'flex-row', 'items-center', 'gap-0', 'h-full')}>
<Drawer storage={storage} logo={logo}>
{items.map((item, index) => (
<DrawerItem
key={index}
href={item.href}
icon={item.icon}
primaryText={item.primaryText}
secondaryText={item.secondaryText}
/>
))}
</Drawer>
<div className={cx('flex', 'flex-col', 'items-center', 'h-full', 'w-full')}>
{topBar}
<div
className={cx(
'flex',
'flex-col',
'w-full',
'lg:w-[1250px]',
'px-3.5',
'pt-3.5',
'overflow-y-auto',
'overflow-x-hidden',
'grow',
)}
>
{children}
</div>
</div>
</div>
)

View File

@@ -0,0 +1,5 @@
import { Spinner as ChakraSpinner } from '@chakra-ui/react'
export const Spinner = (props: any) => (
<ChakraSpinner size="sm" thickness="4px" {...props} />
)

View File

@@ -0,0 +1,123 @@
import {
ChangeEvent,
ReactElement,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import {
IconButton,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
Switch,
} from '@chakra-ui/react'
import cx from 'classnames'
type SwitchCardProps = {
children?: ReactNode
icon: ReactElement
label: string
isCollapsed?: boolean
localStorageNamespace: string
expandedMinWidth?: string
}
export const SwitchCard = ({
children,
icon,
label,
isCollapsed,
localStorageNamespace,
expandedMinWidth,
}: SwitchCardProps) => {
const [isActive, setIsActive] = useState(false)
const localStorageActiveKey = useMemo(
() => `voltaserve_${localStorageNamespace}_switch_card_active`,
[localStorageNamespace],
)
useEffect(() => {
let active = false
if (typeof localStorage !== 'undefined') {
const value = localStorage.getItem(localStorageActiveKey)
if (value) {
active = JSON.parse(value)
} else {
localStorage.setItem(localStorageActiveKey, JSON.stringify(false))
}
}
if (active) {
setIsActive(true)
} else {
setIsActive(false)
}
}, [localStorageActiveKey, setIsActive])
const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
setIsActive(event.target.checked)
localStorage.setItem(
localStorageActiveKey,
JSON.stringify(event.target.checked),
)
},
[localStorageActiveKey],
)
if (isCollapsed) {
return (
<Popover>
<PopoverTrigger>
<IconButton
icon={icon}
variant="outline"
className={cx('w-[50px]', 'h-[50px]', 'p-1.5', 'rounded-md')}
aria-label={label}
title={label}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>{children}</PopoverBody>
</PopoverContent>
</Popover>
)
} else {
return (
<div
className={cx(
'flex',
'flex-col',
'gap-0',
'border',
'border-gray-200',
'dark:border-gray-600',
'rounded-md',
)}
style={{ minWidth: expandedMinWidth }}
>
<div
className={cx(
'flex',
'flex-row',
'items-center',
'gap-1',
'h-[50px]',
'px-1',
'shrink-0',
)}
>
{icon}
<span className={cx('grow')}>{label}</span>
<Switch isChecked={isActive} onChange={handleChange} />
</div>
{isActive && (
<div className={cx('pt-0', 'pr-1', 'pb-1', 'pl-1')}>{children}</div>
)}
</div>
)
}
}

View File

@@ -0,0 +1,47 @@
import React, { ReactNode, useEffect, useRef } from 'react'
import cx from 'classnames'
interface TextProps extends React.HTMLAttributes<HTMLSpanElement> {
children?: ReactNode
noOfLines?: number
maxCharacters?: number
}
export const Text: React.FC<TextProps> = ({
children,
noOfLines,
maxCharacters,
className,
...props
}: TextProps) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const text = children?.toString() || ''
if (ref.current && maxCharacters && text.length > maxCharacters) {
ref.current.textContent = text.slice(0, maxCharacters).trim() + '…'
}
}, [children, maxCharacters])
return (
<span
{...props}
ref={ref}
style={{
display: noOfLines !== undefined ? '-webkit-box' : undefined,
WebkitBoxOrient: noOfLines !== undefined ? 'vertical' : undefined,
WebkitLineClamp: noOfLines,
}}
className={cx(
{ 'whitespace-nowrap': maxCharacters !== undefined },
{
'overflow-hidden':
noOfLines !== undefined || maxCharacters !== undefined,
},
className,
)}
>
{children}
</span>
)
}