Production-ready improvements: security hardening, Redis sessions, retry logic, updated pricing
Changes: - Removed hardcoded admin credentials (now requires env vars) - Added Redis session storage with in-memory fallback - Improved CORS configuration with warnings for development mode - Added retry_with_backoff decorator for translation API calls - Updated pricing: Starter=, Pro=, Business= - Stripe price IDs now loaded from environment variables - Added redis to requirements.txt - Updated .env.example with all new configuration options - Created COMPREHENSIVE_REVIEW_AND_PLAN.md with deployment roadmap - Frontend: Updated pricing page, new UI components
This commit is contained in:
@@ -1,46 +1,295 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
"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 [a&]:hover:bg-primary/90",
|
||||
"border-transparent bg-primary text-primary-foreground shadow-sm hover:shadow-md hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
"border-transparent bg-surface text-secondary-foreground hover:bg-surface-hover",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
"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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
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="mr-1 flex-shrink-0">
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<span className="flex items-center gap-1">
|
||||
{children}
|
||||
{displayCount && (
|
||||
<span className="ml-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="ml-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 (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
<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"
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
// 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, StatusBadge, CounterBadge, ProgressBadge }
|
||||
|
||||
Reference in New Issue
Block a user