Files
office_translator/frontend/src/components/ui/badge.tsx
sepehr ce8e150a61 feat: homelab deployment - NPM + IONOS DNS + monitoring + NAS backup
- Restructured docker-compose for Nginx Proxy Manager (no custom nginx)
- Added domain wordly.art configuration
- Added Prometheus + Grafana monitoring stack with pre-configured dashboards
- Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation)
- Added alert rules for backend, system, and Docker metrics
- Updated deployment guide for NPM + IONOS DNS homelab setup
- Added marketing plan document
- PDF translator and watermark support
- Enhanced middleware, routes, and translator modules

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:43:28 +02:00

298 lines
8.8 KiB
TypeScript

"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-all duration-200 ease-out focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow-sm hover:shadow-md hover:bg-primary/80",
secondary:
"border-transparent bg-surface text-secondary-foreground hover:bg-surface-hover",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80",
outline: "text-foreground border-border hover:bg-surface hover:border-border-strong",
success:
"border-transparent bg-success text-success-foreground shadow-sm hover:bg-success/80",
warning:
"border-transparent bg-warning text-warning-foreground shadow-sm hover:bg-warning/80",
info:
"border-transparent bg-primary/20 text-primary border-primary/30 hover:bg-primary/30",
accent:
"border-transparent bg-accent text-accent-foreground shadow-sm hover:bg-accent/80",
glass:
"glass text-foreground border-border/20 hover:bg-surface/50 hover:border-border/40",
gradient:
"border-transparent bg-gradient-to-r from-primary to-accent text-white shadow-lg hover:shadow-xl hover:shadow-primary/25",
neon:
"border-transparent bg-primary/10 text-primary border-primary/20 shadow-lg shadow-primary/20 hover:bg-primary/20 hover:border-primary/30 hover:shadow-primary/30",
pulse:
"border-transparent bg-primary text-primary-foreground shadow-lg shadow-primary/25 animate-pulse hover:animate-none",
dot:
"border-transparent bg-primary text-primary-foreground w-2 h-2 p-0 rounded-full",
premium:
"border-transparent bg-gradient-to-r from-primary via-accent to-primary text-white shadow-lg hover:shadow-xl hover:shadow-primary/25 relative overflow-hidden",
},
size: {
default: "px-2.5 py-0.5 text-xs",
sm: "px-2 py-0.5 text-xs",
lg: "px-3 py-1 text-sm",
xl: "px-4 py-1.5 text-base",
icon: "w-6 h-6 p-0 rounded-lg flex items-center justify-center",
dot: "w-2 h-2 p-0 rounded-full",
},
interactive: {
true: "cursor-pointer hover:scale-105 active:scale-95",
false: "",
},
},
defaultVariants: {
variant: "default",
size: "default",
interactive: false,
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {
icon?: React.ReactNode
removable?: boolean
onRemove?: () => void
pulse?: boolean
count?: number
maxCount?: number
}
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
({
className,
variant,
size,
interactive = false,
icon,
removable = false,
onRemove,
pulse = false,
count,
maxCount = 99,
children,
...props
}, ref) => {
const [visible, setVisible] = React.useState(true)
const [removing, setRemoving] = React.useState(false)
const handleRemove = () => {
setRemoving(true)
setTimeout(() => {
setVisible(false)
onRemove?.()
}, 200)
}
const displayCount = count && count > maxCount ? `${maxCount}+` : count
if (!visible) return null
return (
<div
ref={ref}
className={cn(
badgeVariants({ variant, size, interactive }),
pulse && "animate-pulse",
removing && "scale-0 opacity-0",
className
)}
{...props}
>
{/* Gradient Shine Effect */}
{(variant === "premium" || variant === "gradient") && (
<span className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -skew-x-12 -translate-x-full group-hover:translate-x-full transition-transform duration-1000 ease-out pointer-events-none" />
)}
{/* Icon */}
{icon && (
<span className="me-1 flex-shrink-0">
{icon}
</span>
)}
{/* Content */}
<span className="flex items-center gap-1">
{children}
{displayCount && (
<span className="ms-1 bg-white/20 px-1.5 py-0.5 rounded text-xs font-bold">
{displayCount}
</span>
)}
</span>
{/* Remove Button */}
{removable && (
<button
type="button"
onClick={handleRemove}
className="ms-1 flex-shrink-0 rounded-full bg-white/20 hover:bg-white/30 transition-colors p-0.5"
aria-label="Remove badge"
>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
{/* Pulse Ring for Neon Variant */}
{variant === "neon" && (
<>
<span className="absolute inset-0 rounded-full bg-primary/20 animate-ping" />
<span className="absolute inset-0 rounded-full bg-primary/10 animate-ping animation-delay-200" />
</>
)}
</div>
)
}
)
Badge.displayName = "Badge"
// Status Badge Component
export const StatusBadge = React.forwardRef<
HTMLDivElement,
Omit<BadgeProps, 'variant'> & {
status: "online" | "offline" | "busy" | "away" | "success" | "error" | "warning"
showLabel?: boolean
}
>(({ className, status, showLabel = true, children, ...props }, ref) => {
const statusConfig = {
online: { variant: "success" as const, label: "Online", icon: "●" },
offline: { variant: "secondary" as const, label: "Offline", icon: "○" },
busy: { variant: "destructive" as const, label: "Busy", icon: "◐" },
away: { variant: "warning" as const, label: "Away", icon: "◐" },
success: { variant: "success" as const, label: "Success", icon: "✓" },
error: { variant: "destructive" as const, label: "Error", icon: "✕" },
warning: { variant: "warning" as const, label: "Warning", icon: "!" },
}
const config = statusConfig[status]
return (
<Badge
ref={ref}
variant={config.variant}
className={cn("gap-1.5", className)}
{...props}
>
<span className="relative">
<span className="text-xs">{config.icon}</span>
{status === "online" && (
<span className="absolute inset-0 rounded-full bg-success animate-ping" />
)}
</span>
{showLabel && (
<span>{children || config.label}</span>
)}
</Badge>
)
})
StatusBadge.displayName = "StatusBadge"
// Counter Badge Component
export const CounterBadge = React.forwardRef<
HTMLDivElement,
Omit<BadgeProps, 'children'> & {
value: number
showZero?: boolean
position?: "top-right" | "top-left" | "bottom-right" | "bottom-left"
}
>(({
className,
value,
showZero = false,
position = "top-right",
size = "icon",
...props
}, ref) => {
if (value === 0 && !showZero) return null
const positionClasses = {
"top-right": "-top-2 -right-2",
"top-left": "-top-2 -left-2",
"bottom-right": "-bottom-2 -right-2",
"bottom-left": "-bottom-2 -left-2",
}
return (
<Badge
ref={ref}
variant="destructive"
size={size}
className={cn(
"absolute min-w-[20px] h-5 flex items-center justify-center text-xs font-bold",
positionClasses[position],
className
)}
{...props}
>
{value > 99 ? "99+" : value}
</Badge>
)
})
CounterBadge.displayName = "CounterBadge"
// Progress Badge Component
export const ProgressBadge = React.forwardRef<
HTMLDivElement,
Omit<BadgeProps, 'children'> & {
value: number
max?: number
showPercentage?: boolean
}
>(({
className,
value,
max = 100,
showPercentage = true,
...props
}, ref) => {
const percentage = Math.min(100, Math.max(0, (value / max) * 100))
const displayValue = showPercentage ? `${Math.round(percentage)}%` : `${value}/${max}`
return (
<Badge
ref={ref}
variant="outline"
className={cn(
"relative overflow-hidden",
className
)}
{...props}
>
{/* Progress Background */}
<span
className="absolute inset-0 bg-primary/20"
style={{ width: `${percentage}%` }}
/>
{/* Text */}
<span className="relative z-10">{displayValue}</span>
</Badge>
)
})
ProgressBadge.displayName = "ProgressBadge"
export { Badge, badgeVariants }