import { useState, useEffect } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { queryClient } from "@/lib/queryClient"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { useAuth } from "@/hooks/useAuth"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import { isUnauthorizedError } from "@/lib/authUtils"; import AuctionCard from "@/components/AuctionCard"; import type { Auction, MediaOutlet } from "@shared/schema"; import { BookOpen, Settings, User, LogOut, Sun, Moon, Monitor, Globe } from "lucide-react"; import { Link } from "wouter"; import Footer from "@/components/Footer"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import LoginModal from "@/components/LoginModal"; export default function Auctions() { const { user, isAuthenticated } = useAuth(); const { toast } = useToast(); const [showBidModal, setShowBidModal] = useState(false); const [selectedAuction, setSelectedAuction] = useState(null); const [bidForm, setBidForm] = useState({ amount: "", qualityScore: "" }); const [isLoginModalOpen, setIsLoginModalOpen] = useState(false); const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light'); const [language, setLanguage] = useState(() => localStorage.getItem('language') || 'en'); useEffect(() => { if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } localStorage.setItem('theme', theme); }, [theme]); useEffect(() => { localStorage.setItem('language', language); }, [language]); const handleThemeChange = (newTheme: string) => { setTheme(newTheme); }; const handleLanguageChange = (newLanguage: string) => { setLanguage(newLanguage); }; const languages = [ { code: 'en', name: 'English' }, { code: 'fr', name: 'Français' }, { code: 'de', name: 'Deutsch' }, { code: 'it', name: 'Italiano' }, { code: 'hi', name: 'हिन्दी' }, { code: 'ar', name: 'العربية' }, { code: 'ja', name: '日本語' }, { code: 'ko', name: '한국어' }, { code: 'zh-TW', name: '繁體中文' }, { code: 'zh-CN', name: '简体中文' }, ]; const handleLogout = async () => { try { const response = await fetch("/api/logout", { method: "POST", credentials: "include", }); if (response.ok) { toast({ title: "Logged Out", description: "You have been successfully logged out.", }); queryClient.invalidateQueries({ queryKey: ["/api/auth/user"] }); } } catch (error) { toast({ title: "Logout Error", description: "An error occurred while logging out.", variant: "destructive", }); } }; const { data: auctions = [], isLoading: auctionsLoading } = useQuery({ queryKey: ["/api/auctions"], }); const { data: mediaOutlets = [] } = useQuery({ queryKey: ["/api/media-outlets"], }); const bidMutation = useMutation({ mutationFn: async (data: { auctionId: string; amount: string; qualityScore: number }) => { await apiRequest("POST", `/api/auctions/${data.auctionId}/bid`, { amount: data.amount, qualityScore: data.qualityScore }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/auctions"] }); toast({ title: "Success", description: "Your bid has been placed successfully.", }); setShowBidModal(false); setBidForm({ amount: "", qualityScore: "" }); }, onError: (error: Error) => { if (isUnauthorizedError(error)) { toast({ title: "Unauthorized", description: "You are logged out. Logging in again...", variant: "destructive", }); setTimeout(() => { window.location.href = "/api/login"; }, 500); return; } toast({ title: "Error", description: "Failed to place bid. Please try again.", variant: "destructive" }); } }); const handleBidClick = (auction: Auction) => { if (!isAuthenticated) { toast({ title: "Login Required", description: "Please log in to place a bid.", variant: "destructive" }); setIsLoginModalOpen(true); return; } setSelectedAuction(auction); setShowBidModal(true); }; const handleBidSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!selectedAuction) return; const amount = parseFloat(bidForm.amount); const qualityScore = parseInt(bidForm.qualityScore); const currentBid = parseFloat(selectedAuction.currentBid || "0"); if (amount <= currentBid) { toast({ title: "Invalid Bid", description: `Bid must be higher than current bid of $${currentBid}`, variant: "destructive" }); return; } if (qualityScore < 1 || qualityScore > 100) { toast({ title: "Invalid Quality Score", description: "Quality score must be between 1 and 100", variant: "destructive" }); return; } bidMutation.mutate({ auctionId: selectedAuction.id, amount: bidForm.amount, qualityScore }); }; const getMediaOutletName = (mediaOutletId: string) => { const outlet = mediaOutlets.find(o => o.id === mediaOutletId); return outlet?.name || "Unknown Outlet"; }; const formatPrice = (price: string) => { const num = parseFloat(price); return `$${num.toLocaleString()}`; }; const getTimeRemaining = (endDate: string | Date) => { const end = new Date(endDate); const now = new Date(); const diff = end.getTime() - now.getTime(); if (diff <= 0) return "Ended"; const days = Math.floor(diff / (1000 * 60 * 60 * 24)); const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); if (days > 0) return `${days}d ${hours}h`; if (hours > 0) return `${hours}h ${minutes}m`; return `${minutes}m`; }; const getStatusBadge = (auction: Auction) => { const timeRemaining = getTimeRemaining(auction.endDate); if (timeRemaining === "Ended") return { text: "Ended", variant: "secondary" as const }; const end = new Date(auction.endDate); const now = new Date(); const hoursLeft = (end.getTime() - now.getTime()) / (1000 * 60 * 60); if (hoursLeft <= 3) return { text: "Ending Soon", variant: "destructive" as const }; return { text: "Active", variant: "default" as const }; }; return (
{/* Header */}
SAPIENS

Media Outlet Auctions

Bid for exclusive editorial rights and content management privileges

{isAuthenticated && user ? ( <> Theme handleThemeChange('light')} className="cursor-pointer" data-testid="theme-light" > Light handleThemeChange('dark')} className="cursor-pointer" data-testid="theme-dark" > Dark handleThemeChange('system')} className="cursor-pointer" data-testid="theme-system" > System Language {languages.map((lang) => ( handleLanguageChange(lang.code)} className="cursor-pointer" data-testid={`language-${lang.code}`} > {lang.name} ))}
{user.firstName} {user.lastName}
My Account Logout
) : ( <> Theme handleThemeChange('light')} className="cursor-pointer" data-testid="theme-light" > Light handleThemeChange('dark')} className="cursor-pointer" data-testid="theme-dark" > Dark handleThemeChange('system')} className="cursor-pointer" data-testid="theme-system" > System Language {languages.map((lang) => ( handleLanguageChange(lang.code)} className="cursor-pointer" data-testid={`language-${lang.code}`} > {lang.name} ))} )}
{/* Auction Explanation */}

How Auctions Work

Bid Amount

Your maximum willingness to pay for editorial control

Quality Score

Platform assessment of your content quality and engagement

Final Ranking

Combination of bid amount and quality score

{/* Auctions Grid */} {auctionsLoading ? (
{Array.from({ length: 6 }).map((_, i) => (
))}
) : auctions.length === 0 ? (

No Active Auctions

There are currently no active media outlet auctions. Check back later for new opportunities!

) : (
{auctions.map((auction) => { const status = getStatusBadge(auction); return (

{getMediaOutletName(auction.mediaOutletId)}

{status.text}
Current Bid: {formatPrice(auction.currentBid || "0")}
Quality Score: {auction.qualityScore || 0}/100
Duration: {auction.duration || 30} days
Time Remaining: {getTimeRemaining(auction.endDate)}
); })}
)}
{/* Bid Modal */} Place Bid {selectedAuction && (

{selectedAuction.title}

{selectedAuction.description}

Current Bid: {formatPrice(selectedAuction.currentBid || "0")}
setBidForm(prev => ({ ...prev, amount: e.target.value }))} onInvalid={(e) => (e.target as HTMLInputElement).setCustomValidity('Please enter a bid amount')} onInput={(e) => (e.target as HTMLInputElement).setCustomValidity('')} placeholder={`Minimum: $${(parseFloat(selectedAuction.currentBid || "0") + 1).toFixed(2)}`} required data-testid="input-bid-amount" />

Must be higher than current bid

setBidForm(prev => ({ ...prev, qualityScore: e.target.value }))} onInvalid={(e) => (e.target as HTMLInputElement).setCustomValidity('Please enter a quality score (1-100)')} onInput={(e) => (e.target as HTMLInputElement).setCustomValidity('')} placeholder="Your self-assessed quality score" required data-testid="input-quality-score" />

Based on your content quality, engagement, and editorial standards

Bidding Formula

Your ranking = (Bid Amount × Quality Score) / 100. Higher scores win at lower costs.

)}
{/* Login Modal */} setIsLoginModalOpen(false)} />
); }