Add media outlet pages with articles and related auction details

Integrates new API endpoints for fetching articles and auction data for each media outlet, displaying them on dedicated pages with article listings and auction information.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 069d4324-6c40-4355-955e-c714a50de1ea
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/069d4324-6c40-4355-955e-c714a50de1ea/jvFIdY3
This commit is contained in:
kimjaehyeon0101
2025-09-29 19:04:38 +00:00
parent 8efbfc51d7
commit df46319424

View File

@ -1,14 +1,16 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useRoute } from "wouter"; import { useRoute, useLocation } from "wouter";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Gavel, Clock, TrendingUp } from "lucide-react";
import ArticleCard from "@/components/ArticleCard"; import ArticleCard from "@/components/ArticleCard";
import type { MediaOutlet, Article } from "@shared/schema"; import type { MediaOutlet, Article, Auction } from "@shared/schema";
export default function MediaOutlet() { export default function MediaOutlet() {
const [, params] = useRoute("/media/:slug"); const [, params] = useRoute("/media/:slug");
const [, setLocation] = useLocation();
const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const { data: outlet, isLoading: outletLoading } = useQuery<MediaOutlet>({ const { data: outlet, isLoading: outletLoading } = useQuery<MediaOutlet>({
@ -17,10 +19,38 @@ export default function MediaOutlet() {
}); });
const { data: articles = [], isLoading: articlesLoading } = useQuery<Article[]>({ const { data: articles = [], isLoading: articlesLoading } = useQuery<Article[]>({
queryKey: ["/api/media-outlets", outlet?.id, "articles"], queryKey: ["/api/media-outlets", params?.slug, "articles"],
enabled: !!outlet?.id enabled: !!params?.slug
}); });
const { data: auction, isLoading: auctionLoading } = useQuery<Auction>({
queryKey: ["/api/media-outlets", params?.slug, "auction"],
enabled: !!params?.slug
});
const formatCurrency = (amount: string | null) => {
if (!amount) return "₩0";
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW'
}).format(parseFloat(amount));
};
const formatTimeRemaining = (endDate: Date | string) => {
const end = new Date(endDate);
const now = new Date();
const diff = end.getTime() - now.getTime();
if (diff <= 0) return "경매 종료";
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
if (days > 0) return `${days}${hours}시간 남음`;
if (hours > 0) return `${hours}시간 남음`;
return "1시간 미만 남음";
};
if (outletLoading) { if (outletLoading) {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
@ -97,7 +127,7 @@ export default function MediaOutlet() {
<div className="flex-1"> <div className="flex-1">
<h1 className="text-3xl font-bold mb-2">{outlet.name}</h1> <h1 className="text-3xl font-bold mb-2">{outlet.name}</h1>
<p className="text-lg text-muted-foreground mb-4">{outlet.description}</p> <p className="text-lg text-muted-foreground mb-4">{outlet.description}</p>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2 mb-4">
<Badge variant="secondary" className="capitalize"> <Badge variant="secondary" className="capitalize">
{outlet.category} {outlet.category}
</Badge> </Badge>
@ -107,6 +137,68 @@ export default function MediaOutlet() {
</Badge> </Badge>
))} ))}
</div> </div>
{/* Auction Section */}
{!auctionLoading && auction ? (
<Card className="border-orange-200 bg-orange-50">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Gavel className="h-5 w-5 text-orange-600" />
<span className="font-semibold text-orange-900"> </span>
</div>
<div className="flex items-center space-x-2 text-sm text-orange-700">
<TrendingUp className="h-4 w-4" />
<span> : {formatCurrency(auction.currentBid)}</span>
</div>
<div className="flex items-center space-x-2 text-sm text-orange-700">
<Clock className="h-4 w-4" />
<span>{formatTimeRemaining(auction.endDate)}</span>
</div>
</div>
<Button
onClick={() => setLocation(`/media/${params?.slug}/auction`)}
size="sm"
className="bg-orange-600 hover:bg-orange-700"
data-testid="button-auction"
>
</Button>
</div>
</CardContent>
</Card>
) : !auctionLoading && !auction ? (
<Card className="border-gray-200 bg-gray-50">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Gavel className="h-5 w-5 text-gray-500" />
<span className="text-gray-700"> </span>
</div>
<Button
variant="outline"
size="sm"
disabled
data-testid="button-no-auction"
>
</Button>
</div>
</CardContent>
</Card>
) : (
<Card className="border-gray-200 bg-gray-50">
<CardContent className="p-4">
<div className="flex items-center space-x-2">
<div className="animate-pulse flex space-x-4">
<div className="rounded-full bg-gray-300 h-5 w-5"></div>
<div className="h-4 bg-gray-300 rounded w-24"></div>
</div>
</div>
</CardContent>
</Card>
)}
</div> </div>
</div> </div>
</div> </div>