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:
325
frontend/src/components/ui/notification.tsx
Normal file
325
frontend/src/components/ui/notification.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X, CheckCircle, AlertCircle, AlertTriangle, Info, Loader2 } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const notificationVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg border p-4 pr-8 shadow-lg transition-all duration-300 ease-out",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-border bg-card text-foreground",
|
||||
destructive: "border-destructive bg-destructive text-destructive-foreground",
|
||||
success: "border-success bg-success text-success-foreground",
|
||||
warning: "border-warning bg-warning text-warning-foreground",
|
||||
info: "border-primary bg-primary text-primary-foreground",
|
||||
glass: "glass text-foreground border-border/20",
|
||||
},
|
||||
size: {
|
||||
default: "max-w-md",
|
||||
sm: "max-w-sm",
|
||||
lg: "max-w-lg",
|
||||
xl: "max-w-xl",
|
||||
full: "max-w-full",
|
||||
},
|
||||
position: {
|
||||
"top-right": "fixed top-4 right-4 z-50",
|
||||
"top-left": "fixed top-4 left-4 z-50",
|
||||
"bottom-right": "fixed bottom-4 right-4 z-50",
|
||||
"bottom-left": "fixed bottom-4 left-4 z-50",
|
||||
"top-center": "fixed top-4 left-1/2 transform -translate-x-1/2 z-50",
|
||||
"bottom-center": "fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50",
|
||||
"center": "fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
position: "top-right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface NotificationProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof notificationVariants> {
|
||||
title?: string
|
||||
description?: string
|
||||
action?: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
loading?: boolean
|
||||
closable?: boolean
|
||||
autoClose?: boolean
|
||||
duration?: number
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
|
||||
({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
position,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
icon,
|
||||
loading = false,
|
||||
closable = true,
|
||||
autoClose = true,
|
||||
duration = 5000,
|
||||
onClose,
|
||||
children,
|
||||
...props
|
||||
}, ref) => {
|
||||
const [visible, setVisible] = React.useState(true)
|
||||
const [progress, setProgress] = React.useState(100)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (autoClose && !loading) {
|
||||
const startTime = Date.now()
|
||||
const interval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime
|
||||
const remaining = Math.max(0, 100 - (elapsed / duration) * 100)
|
||||
setProgress(remaining)
|
||||
|
||||
if (remaining === 0) {
|
||||
clearInterval(interval)
|
||||
setVisible(false)
|
||||
onClose?.()
|
||||
}
|
||||
}, 50)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [autoClose, loading, duration, onClose])
|
||||
|
||||
const handleClose = () => {
|
||||
setVisible(false)
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
const defaultIcons = {
|
||||
default: <Info className="h-5 w-5" />,
|
||||
destructive: <AlertCircle className="h-5 w-5" />,
|
||||
success: <CheckCircle className="h-5 w-5" />,
|
||||
warning: <AlertTriangle className="h-5 w-5" />,
|
||||
info: <Info className="h-5 w-5" />,
|
||||
glass: <Info className="h-5 w-5" />,
|
||||
}
|
||||
|
||||
const displayIcon = icon || defaultIcons[variant as keyof typeof defaultIcons] || defaultIcons.default
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(notificationVariants({ variant, size, position }), className)}
|
||||
{...props}
|
||||
>
|
||||
{/* Progress Bar for Auto-close */}
|
||||
{autoClose && !loading && (
|
||||
<div className="absolute top-0 left-0 h-1 bg-white/20">
|
||||
<div
|
||||
className="h-full bg-white/40 transition-all duration-100 ease-linear"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Icon */}
|
||||
<div className={cn(
|
||||
"flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center",
|
||||
variant === "success" && "bg-success/20 text-success",
|
||||
variant === "destructive" && "bg-destructive/20 text-destructive",
|
||||
variant === "warning" && "bg-warning/20 text-warning",
|
||||
variant === "info" && "bg-primary/20 text-primary",
|
||||
variant === "default" && "bg-muted text-muted-foreground",
|
||||
variant === "glass" && "bg-surface/50 text-foreground"
|
||||
)}>
|
||||
{loading ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
displayIcon
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="grid gap-1 flex-1 min-w-0">
|
||||
{title && (
|
||||
<div className="text-sm font-semibold leading-none">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{description && (
|
||||
<div className="text-sm opacity-90 leading-relaxed">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action */}
|
||||
{action && (
|
||||
<div className="flex-shrink-0">
|
||||
{action}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Close Button */}
|
||||
{closable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="absolute right-2 top-2 flex-shrink-0 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
aria-label="Close notification"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Notification.displayName = "Notification"
|
||||
|
||||
// Notification Context
|
||||
interface NotificationContextType {
|
||||
notifications: Array<{
|
||||
id: string
|
||||
title?: string
|
||||
description?: string
|
||||
variant?: VariantProps<typeof notificationVariants>["variant"]
|
||||
duration?: number
|
||||
action?: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
closable?: boolean
|
||||
autoClose?: boolean
|
||||
}>
|
||||
notify: (notification: Omit<NotificationContextType["notifications"][0], "id">) => void
|
||||
success: (notification: Omit<NotificationContextType["notifications"][0], "variant">) => void
|
||||
error: (notification: Omit<NotificationContextType["notifications"][0], "variant">) => void
|
||||
warning: (notification: Omit<NotificationContextType["notifications"][0], "variant">) => void
|
||||
info: (notification: Omit<NotificationContextType["notifications"][0], "variant">) => void
|
||||
dismiss: (id: string) => void
|
||||
dismissAll: () => void
|
||||
}
|
||||
|
||||
const NotificationContext = React.createContext<NotificationContextType | undefined>(undefined)
|
||||
|
||||
export function NotificationProvider({ children }: { children: React.ReactNode }) {
|
||||
const [notifications, setNotifications] = React.useState<NotificationContextType["notifications"]>([])
|
||||
|
||||
const notify = React.useCallback(
|
||||
(notification: Omit<NotificationContextType["notifications"][0], "id">) => {
|
||||
const id = Math.random().toString(36).substr(2, 9)
|
||||
setNotifications(prev => [...prev, { ...notification, id }])
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const success = React.useCallback(
|
||||
(notification: Omit<NotificationContextType["notifications"][0], "variant">) =>
|
||||
notify({ ...notification, variant: "success" }),
|
||||
[notify]
|
||||
)
|
||||
|
||||
const error = React.useCallback(
|
||||
(notification: Omit<NotificationContextType["notifications"][0], "variant">) =>
|
||||
notify({ ...notification, variant: "destructive" }),
|
||||
[notify]
|
||||
)
|
||||
|
||||
const warning = React.useCallback(
|
||||
(notification: Omit<NotificationContextType["notifications"][0], "variant">) =>
|
||||
notify({ ...notification, variant: "warning" }),
|
||||
[notify]
|
||||
)
|
||||
|
||||
const info = React.useCallback(
|
||||
(notification: Omit<NotificationContextType["notifications"][0], "variant">) =>
|
||||
notify({ ...notification, variant: "info" }),
|
||||
[notify]
|
||||
)
|
||||
|
||||
const dismiss = React.useCallback((id: string) => {
|
||||
setNotifications(prev => prev.filter(n => n.id !== id))
|
||||
}, [])
|
||||
|
||||
const dismissAll = React.useCallback(() => {
|
||||
setNotifications([])
|
||||
}, [])
|
||||
|
||||
const value = React.useMemo(() => ({
|
||||
notifications,
|
||||
notify,
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
dismiss,
|
||||
dismissAll,
|
||||
}), [notifications, notify, success, error, warning, info, dismiss, dismissAll])
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={value}>
|
||||
{children}
|
||||
<NotificationContainer />
|
||||
</NotificationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useNotification() {
|
||||
const context = React.useContext(NotificationContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useNotification must be used within a NotificationProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// Notification Container
|
||||
function NotificationContainer() {
|
||||
const { notifications, dismiss } = useNotification()
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 right-0 z-50 flex flex-col-reverse p-4 space-y-2 pointer-events-none">
|
||||
{notifications.map((notification) => (
|
||||
<Notification
|
||||
key={notification.id}
|
||||
position="top-right"
|
||||
variant={notification.variant}
|
||||
title={notification.title}
|
||||
description={notification.description}
|
||||
action={notification.action}
|
||||
icon={notification.icon}
|
||||
closable={notification.closable}
|
||||
autoClose={notification.autoClose}
|
||||
duration={notification.duration}
|
||||
onClose={() => dismiss(notification.id)}
|
||||
className="pointer-events-auto animate-in slide-in-from-top-2"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Skeleton Notification for Loading States
|
||||
export const NotificationSkeleton = () => (
|
||||
<div className="w-full max-w-md rounded-lg border border-border bg-card p-4 shadow-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-border skeleton" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-4 w-3/4 rounded bg-border skeleton" />
|
||||
<div className="h-3 w-1/2 rounded bg-border skeleton" />
|
||||
</div>
|
||||
<div className="w-6 h-6 rounded bg-border skeleton" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export { Notification, notificationVariants }
|
||||
Reference in New Issue
Block a user