Admin Panel: - User Detail Modal with full profile info - Bulk tier upgrade for multiple users - User export to CSV - Price Alerts overview tab - Domain Health Check trigger - Email Test functionality - Scheduler Status with job info and last runs - Activity Log for admin actions - Blog management tab with CRUD Blog System: - BlogPost model with full content management - Public API: list, featured, categories, single post - Admin API: create, update, delete, publish/unpublish - Frontend blog listing page with categories - Frontend blog detail page with styling - View count tracking OAuth: - Google OAuth integration - GitHub OAuth integration - OAuth callback handling - Provider selection on login/register Other improvements: - Domain checker with check_all_domains function - Admin activity logging - Breadcrumbs component - Toast notification component - Various UI/UX improvements
95 lines
2.7 KiB
TypeScript
95 lines
2.7 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { Check, X, AlertCircle, Info } from 'lucide-react'
|
|
import clsx from 'clsx'
|
|
|
|
export type ToastType = 'success' | 'error' | 'info'
|
|
|
|
interface ToastProps {
|
|
message: string
|
|
type?: ToastType
|
|
duration?: number
|
|
onClose: () => void
|
|
}
|
|
|
|
export function Toast({ message, type = 'success', duration = 4000, onClose }: ToastProps) {
|
|
const [isVisible, setIsVisible] = useState(true)
|
|
const [isLeaving, setIsLeaving] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
setIsLeaving(true)
|
|
setTimeout(onClose, 300)
|
|
}, duration)
|
|
|
|
return () => clearTimeout(timer)
|
|
}, [duration, onClose])
|
|
|
|
const handleClose = () => {
|
|
setIsLeaving(true)
|
|
setTimeout(onClose, 300)
|
|
}
|
|
|
|
const Icon = type === 'success' ? Check : type === 'error' ? AlertCircle : Info
|
|
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
"fixed bottom-6 right-6 z-[100] flex items-center gap-3 px-4 py-3 rounded-xl shadow-2xl border transition-all duration-300",
|
|
isLeaving ? "translate-y-2 opacity-0" : "translate-y-0 opacity-100",
|
|
type === 'success' && "bg-accent/10 border-accent/20",
|
|
type === 'error' && "bg-danger/10 border-danger/20",
|
|
type === 'info' && "bg-foreground/5 border-border"
|
|
)}
|
|
>
|
|
<div className={clsx(
|
|
"w-7 h-7 rounded-lg flex items-center justify-center",
|
|
type === 'success' && "bg-accent/20",
|
|
type === 'error' && "bg-danger/20",
|
|
type === 'info' && "bg-foreground/10"
|
|
)}>
|
|
<Icon className={clsx(
|
|
"w-4 h-4",
|
|
type === 'success' && "text-accent",
|
|
type === 'error' && "text-danger",
|
|
type === 'info' && "text-foreground-muted"
|
|
)} />
|
|
</div>
|
|
<p className={clsx(
|
|
"text-body-sm",
|
|
type === 'success' && "text-accent",
|
|
type === 'error' && "text-danger",
|
|
type === 'info' && "text-foreground"
|
|
)}>{message}</p>
|
|
<button
|
|
onClick={handleClose}
|
|
className={clsx(
|
|
"ml-2 p-1 rounded hover:bg-foreground/5 transition-colors",
|
|
type === 'success' && "text-accent/70 hover:text-accent",
|
|
type === 'error' && "text-danger/70 hover:text-danger",
|
|
type === 'info' && "text-foreground-muted hover:text-foreground"
|
|
)}
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Hook for managing toasts
|
|
export function useToast() {
|
|
const [toast, setToast] = useState<{ message: string; type: ToastType } | null>(null)
|
|
|
|
const showToast = (message: string, type: ToastType = 'success') => {
|
|
setToast({ message, type })
|
|
}
|
|
|
|
const hideToast = () => {
|
|
setToast(null)
|
|
}
|
|
|
|
return { toast, showToast, hideToast }
|
|
}
|
|
|