Widget komponenti
This commit is contained in:
parent
0f12b2e030
commit
391428d874
2 changed files with 1703 additions and 0 deletions
90
ui/src/components/ui/Widget/Widget.tsx
Normal file
90
ui/src/components/ui/Widget/Widget.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { iconList } from './iconList'
|
||||||
|
|
||||||
|
export type colorType =
|
||||||
|
| 'blue'
|
||||||
|
| 'green'
|
||||||
|
| 'purple'
|
||||||
|
| 'gray'
|
||||||
|
| 'red'
|
||||||
|
| 'yellow'
|
||||||
|
| 'pink'
|
||||||
|
| 'indigo'
|
||||||
|
| 'teal'
|
||||||
|
| 'orange'
|
||||||
|
|
||||||
|
interface WidgetProps {
|
||||||
|
title: string
|
||||||
|
value: string | number
|
||||||
|
valueClassName?: string
|
||||||
|
color: colorType
|
||||||
|
icon: (typeof iconList)[number]
|
||||||
|
subtitle?: string
|
||||||
|
onClick?: () => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Widget({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
valueClassName = 'text-3xl',
|
||||||
|
color,
|
||||||
|
icon,
|
||||||
|
subtitle,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
}: WidgetProps) {
|
||||||
|
const [IconComponent, setIconComponent] = useState<React.ComponentType<{
|
||||||
|
className?: string
|
||||||
|
}> | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true
|
||||||
|
import('react-icons/fa').then((icons) => {
|
||||||
|
if (isMounted && icon in icons) {
|
||||||
|
setIconComponent(() => (icons as any)[icon])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
isMounted = false
|
||||||
|
}
|
||||||
|
}, [icon])
|
||||||
|
|
||||||
|
const colorMap: Record<string, { bg: string; text: string }> = {
|
||||||
|
blue: { bg: 'from-blue-100 to-blue-200', text: 'text-blue-600' },
|
||||||
|
green: { bg: 'from-green-100 to-green-200', text: 'text-green-600' },
|
||||||
|
purple: { bg: 'from-purple-100 to-purple-200', text: 'text-purple-600' },
|
||||||
|
gray: { bg: 'from-gray-100 to-gray-200', text: 'text-gray-600' },
|
||||||
|
red: { bg: 'from-red-100 to-red-200', text: 'text-red-600' },
|
||||||
|
yellow: { bg: 'from-yellow-100 to-yellow-200', text: 'text-yellow-600' },
|
||||||
|
pink: { bg: 'from-pink-100 to-pink-200', text: 'text-pink-600' },
|
||||||
|
indigo: { bg: 'from-indigo-100 to-indigo-200', text: 'text-indigo-600' },
|
||||||
|
teal: { bg: 'from-teal-100 to-teal-200', text: 'text-teal-600' },
|
||||||
|
orange: { bg: 'from-orange-100 to-orange-200', text: 'text-orange-600' },
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
className={classNames(
|
||||||
|
'bg-white rounded-lg shadow-sm border border-gray-200 p-4 flex flex-col justify-between',
|
||||||
|
onClick && 'cursor-pointer hover:bg-gray-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-gray-600 uppercase tracking-wide">{title}</p>
|
||||||
|
<p className={`${valueClassName} font-bold mt-1 ${colorMap[color].text}`}>{value}</p>
|
||||||
|
{subtitle && <p className="text-sm text-gray-500 mt-1">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`w-12 h-12 bg-gradient-to-br ${colorMap[color].bg} rounded-xl flex items-center justify-center shadow-sm`}
|
||||||
|
>
|
||||||
|
{IconComponent ? <IconComponent className={`w-6 h-6 ${colorMap[color].text}`} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
1613
ui/src/components/ui/Widget/iconList.ts
Normal file
1613
ui/src/components/ui/Widget/iconList.ts
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue