Files
sapiens-web2/client/src/pages/Auctions.tsx
kimjaehyeon0101 716361ef96 Update warning messages for all input fields to English
Translate various input validation messages from Korean to English across LoginModal, Auctions, and Landing components.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 069d4324-6c40-4355-955e-c714a50de1ea
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/069d4324-6c40-4355-955e-c714a50de1ea/9tQ591o
2025-09-29 21:46:54 +00:00

408 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } 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 } from "lucide-react";
import { Link } from "wouter";
import Footer from "@/components/Footer";
export default function Auctions() {
const { user, isAuthenticated } = useAuth();
const { toast } = useToast();
const [showBidModal, setShowBidModal] = useState(false);
const [selectedAuction, setSelectedAuction] = useState<Auction | null>(null);
const [bidForm, setBidForm] = useState({
amount: "",
qualityScore: ""
});
const { data: auctions = [], isLoading: auctionsLoading } = useQuery<Auction[]>({
queryKey: ["/api/auctions"],
});
const { data: mediaOutlets = [] } = useQuery<MediaOutlet[]>({
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"
});
setTimeout(() => {
window.location.href = "/api/login";
}, 1000);
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 (
<div className="flex flex-col min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/">
<img
src="/attached_assets/logo_black_1759181850935.png"
alt="SAPIENS"
className="h-5 w-auto cursor-pointer"
data-testid="logo-sapiens"
/>
</Link>
<div>
<h1 className="text-base font-bold">Media Outlet Auctions</h1>
<p className="text-xs text-gray-600">Bid for exclusive editorial rights and content management privileges</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Link href="/auction-guide">
<Button variant="ghost" size="sm" data-testid="button-auction-guide">
<BookOpen className="h-4 w-4" />
</Button>
</Link>
{isAuthenticated && user ? (
<>
{(user.role === 'admin' || user.role === 'superadmin') && (
<Link href="/admin">
<Button variant="ghost" size="sm">
Admin Dashboard
</Button>
</Link>
)}
<Button
variant="ghost"
size="sm"
onClick={() => window.location.href = "/api/logout"}
>
Logout
</Button>
</>
) : (
<Button size="sm" onClick={() => window.location.href = "/api/login"}>
Login
</Button>
)}
</div>
</div>
</div>
</header>
<main className="flex-1 max-w-7xl mx-auto px-4 py-3 w-full">
{/* Auction Explanation */}
<div className="mb-4">
<Card>
<CardContent className="p-3">
<h3 className="font-semibold mb-2 text-sm">How Auctions Work</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-xs">
<div>
<h4 className="font-medium mb-1">Bid Amount</h4>
<p className="text-gray-600">Your maximum willingness to pay for editorial control</p>
</div>
<div>
<h4 className="font-medium mb-1">Quality Score</h4>
<p className="text-gray-600">Platform assessment of your content quality and engagement</p>
</div>
<div>
<h4 className="font-medium mb-1">Final Ranking</h4>
<p className="text-gray-600">Combination of bid amount and quality score</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Auctions Grid */}
{auctionsLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<Card key={i}>
<CardContent className="p-6 animate-pulse">
<div className="h-20 bg-muted rounded mb-4"></div>
<div className="space-y-3">
<div className="h-4 bg-muted rounded"></div>
<div className="h-4 bg-muted rounded"></div>
<div className="h-4 bg-muted rounded"></div>
</div>
<div className="h-10 bg-muted rounded mt-4"></div>
</CardContent>
</Card>
))}
</div>
) : auctions.length === 0 ? (
<Card>
<CardContent className="p-12 text-center">
<i className="fas fa-gavel text-4xl text-muted-foreground mb-4"></i>
<h3 className="text-xl font-semibold mb-2">No Active Auctions</h3>
<p className="text-muted-foreground">
There are currently no active media outlet auctions. Check back later for new opportunities!
</p>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{auctions.map((auction) => {
const status = getStatusBadge(auction);
return (
<Card key={auction.id} data-testid={`card-auction-${auction.id}`}>
<CardContent className="p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="font-semibold text-lg line-clamp-1">{getMediaOutletName(auction.mediaOutletId)}</h3>
</div>
<Badge variant={status.variant}>{status.text}</Badge>
</div>
<div className="space-y-3 mb-4">
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Current Bid:</span>
<span className="font-semibold">{formatPrice(auction.currentBid || "0")}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Quality Score:</span>
<span className="font-semibold text-chart-2">{auction.qualityScore || 0}/100</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Duration:</span>
<span className="font-semibold">{auction.duration || 30} days</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Time Remaining:</span>
<span className={`font-semibold ${status.variant === 'destructive' ? 'text-destructive' : ''}`}>
{getTimeRemaining(auction.endDate)}
</span>
</div>
</div>
<Button
className="w-full"
onClick={() => handleBidClick(auction)}
disabled={getTimeRemaining(auction.endDate) === "Ended"}
data-testid={`button-bid-${auction.id}`}
>
{getTimeRemaining(auction.endDate) === "Ended" ? "Auction Ended" : "Place Bid"}
</Button>
</CardContent>
</Card>
);
})}
</div>
)}
</main>
{/* Bid Modal */}
<Dialog open={showBidModal} onOpenChange={setShowBidModal}>
<DialogContent data-testid="modal-bid">
<DialogHeader>
<DialogTitle>Place Bid</DialogTitle>
</DialogHeader>
{selectedAuction && (
<div className="space-y-4">
<div className="bg-muted rounded-lg p-4">
<h4 className="font-semibold mb-2">{selectedAuction.title}</h4>
<p className="text-sm text-muted-foreground mb-3">{selectedAuction.description}</p>
<div className="flex justify-between text-sm">
<span>Current Bid:</span>
<span className="font-semibold">{formatPrice(selectedAuction.currentBid || "0")}</span>
</div>
</div>
<form onSubmit={handleBidSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Bid Amount ($)</label>
<Input
type="number"
step="0.01"
min={parseFloat(selectedAuction.currentBid || "0") + 1}
value={bidForm.amount}
onChange={(e) => 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"
/>
<p className="text-xs text-muted-foreground mt-1">
Must be higher than current bid
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">Quality Score (1-100)</label>
<Input
type="number"
min="1"
max="100"
value={bidForm.qualityScore}
onChange={(e) => 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"
/>
<p className="text-xs text-muted-foreground mt-1">
Based on your content quality, engagement, and editorial standards
</p>
</div>
<div className="bg-accent/50 rounded-lg p-3 text-sm">
<h5 className="font-medium mb-1">Bidding Formula</h5>
<p className="text-muted-foreground">
Your ranking = (Bid Amount × Quality Score) / 100. Higher scores win at lower costs.
</p>
</div>
<div className="flex space-x-2">
<Button
type="submit"
disabled={bidMutation.isPending}
data-testid="button-submit-bid"
>
{bidMutation.isPending ? "Placing Bid..." : "Place Bid"}
</Button>
<Button
type="button"
variant="outline"
onClick={() => setShowBidModal(false)}
data-testid="button-cancel-bid"
>
Cancel
</Button>
</div>
</form>
</div>
)}
</DialogContent>
</Dialog>
<Footer />
</div>
);
}