- 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>
298 lines
8.8 KiB
TypeScript
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 }
|