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

View File

@@ -0,0 +1,2 @@
export * from './page-pagination'
export * from './page-monitor'

View File

@@ -0,0 +1,16 @@
export type UsePageMonitorMonitorOptions = {
totalPages: number
totalElements: number
steps: number[]
}
export const usePageMonitor = ({
totalPages,
totalElements,
steps,
}: UsePageMonitorMonitorOptions) => {
const hasPageSwitcher = totalPages > 1
const hasSizeSelector = totalElements > steps[0]
return { hasPageSwitcher, hasSizeSelector }
}

View File

@@ -0,0 +1,77 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
export type NavigateArgs = {
search: string
}
export type NavigateFunction = (args: NavigateArgs) => void
export type LocationObject = {
search: string
}
export type StorageOptions = {
enabled?: boolean
prefix?: string
namespace?: string
}
export type UsePagePaginationOptions = {
navigate: NavigateFunction
location: LocationObject
storage?: StorageOptions
steps?: number[]
}
export const usePagePagination = ({
navigate,
location,
storage = {
enabled: false,
prefix: 'app',
namespace: 'main',
},
steps = [5, 10, 20, 40, 80, 100],
}: UsePagePaginationOptions) => {
const queryParams = useMemo(
() => new URLSearchParams(location.search),
[location.search],
)
const page = Number(queryParams.get('page')) || 1
const storageSizeKey = useMemo(
() => `${storage.prefix}_${storage.namespace}_pagination_size`,
[storage],
)
const [size, setSize] = useState(
localStorage.getItem(storageSizeKey) && storage.enabled
? parseInt(localStorage.getItem(storageSizeKey) as string)
: steps[0],
)
const _setSize = useCallback(
(size: number) => {
setSize(size)
if (size && storage.enabled) {
localStorage.setItem(storageSizeKey, JSON.stringify(size))
}
},
[storageSizeKey, storage],
)
useEffect(() => {
if (!queryParams.has('page')) {
queryParams.set('page', '1')
navigate({ search: `?${queryParams.toString()}` })
}
}, [queryParams, navigate])
const setPage = useCallback(
(page: number) => {
queryParams.set('page', String(page))
navigate({ search: `?${queryParams.toString()}` })
},
[queryParams, navigate],
)
return { page, size, steps, setPage, setSize: _setSize }
}

View File

@@ -0,0 +1,4 @@
export * from './theme'
export * from './variables'
export * from './components'
export * from './hooks'

View File

@@ -0,0 +1,9 @@
const breakpoints = {
sm: '320px',
md: '768px',
lg: '960px',
xl: '1200px',
'2xl': '1536px',
}
export default breakpoints

View File

@@ -0,0 +1,16 @@
const colors = {
blue: {
'50': '#E7EDFE',
'100': '#BBCDFC',
'200': '#8FACF9',
'300': '#648CF7',
'400': '#386CF5',
'500': '#0C4CF3',
'600': '#0A3CC2',
'700': '#072D92',
'800': '#051E61',
'900': '#020F31',
},
}
export default colors

View File

@@ -0,0 +1,14 @@
const breadcrumb = {
baseStyle: {
link: {
_active: {
boxShadow: 'none',
},
_focus: {
boxShadow: 'none',
},
},
},
}
export default breadcrumb

View File

@@ -0,0 +1,27 @@
import { mode, StyleFunctionProps } from '@chakra-ui/theme-tools'
import { variables } from '../../variables'
const button = {
baseStyle: {
borderRadius: variables.borderRadiusMd,
fontWeight: variables.bodyFontWeight,
},
sizes: {
md: {
fontSize: variables.bodyFontSize,
},
xs: {
fontSize: '12px',
},
},
variants: {
'solid-gray': (props: StyleFunctionProps) => ({
bg: mode('gray.100', 'gray.700')(props),
_hover: {
bg: mode('gray.200', 'gray.600')(props),
},
}),
},
}
export default button

View File

@@ -0,0 +1,19 @@
import { variables } from '../../variables'
const checkbox = {
baseStyle: {
control: {
borderRadius: '50%',
},
},
sizes: {
md: {
control: { w: '20px', h: '20px' },
label: {
fontSize: variables.bodyFontSize,
},
},
},
}
export default checkbox

View File

@@ -0,0 +1,10 @@
import { variables } from '../../variables'
const heading = {
baseStyle: {
fontFamily: variables.headingFontFamily,
fontWeight: variables.headingFontWeight,
},
}
export default heading

View File

@@ -0,0 +1,14 @@
import { variables } from '../../variables'
const input = {
sizes: {
md: {
field: {
fontSize: variables.bodyFontSize,
borderRadius: variables.borderRadiusMd,
},
},
},
}
export default input

View File

@@ -0,0 +1,21 @@
const link = {
baseStyle: {
textDecoration: 'underline',
_active: {
boxShadow: 'none',
},
_focus: {
boxShadow: 'none',
},
},
variants: {
'no-underline': {
textDecoration: 'none',
_hover: {
textDecoration: 'none',
},
},
},
}
export default link

View File

@@ -0,0 +1,13 @@
const menu = {
baseStyle: {
list: {
borderRadius: '15px',
py: '12px',
},
item: {
px: '12px',
},
},
}
export default menu

View File

@@ -0,0 +1,14 @@
import { variables } from '../../variables'
const modal = {
baseStyle: {
dialog: {
borderRadius: variables.borderRadius,
},
closeButton: {
borderRadius: '50%',
},
},
}
export default modal

View File

@@ -0,0 +1,21 @@
import { variables } from '../../variables'
const popover = {
baseStyle: {
content: {
borderRadius: '15px',
padding: variables.spacingXs,
boxShadow: 'none',
_focus: {
boxShadow: 'none',
},
},
closeButton: {
borderRadius: '50%',
top: '10px',
right: '10px',
},
},
}
export default popover

View File

@@ -0,0 +1,37 @@
import { progressAnatomy as parts } from '@chakra-ui/anatomy'
import {
mode,
PartsStyleFunction,
StyleFunctionProps,
SystemStyleFunction,
SystemStyleObject,
} from '@chakra-ui/theme-tools'
import { variables } from '../../variables'
function filledStyle(props: StyleFunctionProps): SystemStyleObject {
const { colorScheme, hasStripe } = props
if (hasStripe) {
return { bg: variables.gradiant }
} else {
return { bgColor: mode(`${colorScheme}.500`, `${colorScheme}.200`)(props) }
}
}
const baseStyleFilledTrack: SystemStyleFunction = (props: any) => {
return {
...filledStyle(props),
}
}
const baseStyle: PartsStyleFunction<typeof parts> = (props: any) => ({
filledTrack: baseStyleFilledTrack(props),
track: {
borderRadius: variables.borderRadius,
},
})
const progress = {
baseStyle,
}
export default progress

View File

@@ -0,0 +1,14 @@
import { variables } from '../../variables'
const select = {
sizes: {
md: {
field: {
fontSize: variables.bodyFontSize,
borderRadius: variables.borderRadiusMd,
},
},
},
}
export default select

View File

@@ -0,0 +1,31 @@
import { mode, StyleFunctionProps } from '@chakra-ui/theme-tools'
import { variables } from '../../variables'
const tab = {
variants: {
'solid-rounded': (props: StyleFunctionProps) => ({
tab: {
fontSize: variables.bodyFontSize,
_focus: {
boxShadow: 'none',
},
_selected: {
bg: mode('black', 'white')(props),
},
},
tabpanel: {
p: '60px 0 0 0',
},
}),
'line': {
tab: {
fontSize: variables.bodyFontSize,
_focus: {
boxShadow: 'none',
},
},
},
},
}
export default tab

View File

@@ -0,0 +1,12 @@
import { variables } from '../../variables'
const textarea = {
sizes: {
md: {
fontSize: variables.bodyFontSize,
borderRadius: variables.borderRadiusSm,
},
},
}
export default textarea

View File

@@ -0,0 +1,10 @@
import { variables } from '../../variables'
const tooltip = {
baseStyle: {
borderRadius: variables.borderRadius,
padding: '5px 15px 5px 15px',
},
}
export default tooltip

View File

@@ -0,0 +1,44 @@
import { extendTheme } from '@chakra-ui/react'
import breakpoints from './breakpoints'
import colors from './colors'
import Breadcrumb from './components/breadcrumb'
import Button from './components/button'
import Checkbox from './components/checkbox'
import Heading from './components/heading'
import Input from './components/input'
import Link from './components/link'
import Menu from './components/menu'
import Modal from './components/modal'
import Popover from './components/popover'
import Progress from './components/progress'
import Select from './components/select'
import Tabs from './components/tabs'
import Textarea from './components/textarea'
import Tooltip from './components/tooltip'
import styles from './styles'
import typography from './typography'
const overrides = {
breakpoints,
styles,
colors,
...typography,
components: {
Button,
Heading,
Checkbox,
Select,
Input,
Textarea,
Modal,
Link,
Progress,
Tabs,
Tooltip,
Popover,
Breadcrumb,
Menu,
},
}
export const theme = extendTheme(overrides)

View File

@@ -0,0 +1,13 @@
import { variables } from '../variables'
const styles = {
global: {
body: {
fontFamily: variables.bodyFontFamily,
fontSize: variables.bodyFontSize,
fontWeight: variables.bodyFontWeight,
},
},
}
export default styles

View File

@@ -0,0 +1,10 @@
const typography = {
fontSizes: {
xl: '18px',
lg: '16px',
md: '14px',
sm: '12px',
},
}
export default typography

View File

@@ -0,0 +1,4 @@
export type StorageOptions = {
prefix?: string
namespace?: string
}

View File

@@ -0,0 +1,23 @@
module.exports = {
variables: {
headingFontFamily:
"'Unbounded', Almarai, 'Noto Sans JP', 'Noto Sans TC', 'Noto Sans SC', 'Noto Sans KR', Poppins, 'Noto Sans Bengali'",
headingFontSize: '24px',
headingFontWeight: '500',
bodyFontFamily: 'IBM Plex Sans',
bodyFontSize: '14px',
bodyFontWeight: '400',
spacingXs: '5px',
spacingSm: '10px',
spacing: '15px',
spacingMd: '20px',
spacingLg: '25px',
spacingXl: '30px',
spacing2Xl: '40px',
borderRadiusMd: '30px',
borderRadius: '20px',
borderRadiusSm: '10px',
borderRadiusXs: '5px',
gradiant: 'linear-gradient(90deg, #00c9ff 0%, #92fe9d 100%)',
},
}

View File

@@ -0,0 +1,21 @@
export const variables = {
headingFontFamily:
"'Unbounded', Almarai, 'Noto Sans JP', 'Noto Sans TC', 'Noto Sans SC', 'Noto Sans KR', Poppins, 'Noto Sans Bengali'",
headingFontSize: '24px',
headingFontWeight: '500',
bodyFontFamily: 'IBM Plex Sans',
bodyFontSize: '14px',
bodyFontWeight: '400',
spacingXs: '5px',
spacingSm: '10px',
spacing: '15px',
spacingMd: '20px',
spacingLg: '25px',
spacingXl: '30px',
spacing2Xl: '40px',
borderRadiusMd: '30px',
borderRadius: '20px',
borderRadiusSm: '10px',
borderRadiusXs: '5px',
gradiant: 'linear-gradient(90deg, #00c9ff 0%, #92fe9d 100%)',
}