Files
sapiens-web3/client/src/pages/MediaOutlet.tsx
kimjaehyeon0101 df46319424 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
2025-09-29 19:04:38 +00:00

269 lines
11 KiB
TypeScript

import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useRoute, useLocation } from "wouter";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Gavel, Clock, TrendingUp } from "lucide-react";
import ArticleCard from "@/components/ArticleCard";
import type { MediaOutlet, Article, Auction } from "@shared/schema";
export default function MediaOutlet() {
const [, params] = useRoute("/media/:slug");
const [, setLocation] = useLocation();
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const { data: outlet, isLoading: outletLoading } = useQuery<MediaOutlet>({
queryKey: ["/api/media-outlets", params?.slug],
enabled: !!params?.slug
});
const { data: articles = [], isLoading: articlesLoading } = useQuery<Article[]>({
queryKey: ["/api/media-outlets", params?.slug, "articles"],
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) {
return (
<div className="min-h-screen bg-background">
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="animate-pulse">
<div className="h-8 bg-muted rounded w-1/3 mb-4"></div>
<div className="h-4 bg-muted rounded w-2/3 mb-8"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-64 bg-muted rounded-xl"></div>
))}
</div>
</div>
</div>
</div>
);
}
if (!outlet) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-2">Media Outlet Not Found</h1>
<p className="text-muted-foreground mb-4">The media outlet you're looking for doesn't exist.</p>
<Button onClick={() => window.history.back()}>Go Back</Button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="bg-card border-b border-border sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center text-primary-foreground font-bold text-lg">
S
</div>
<span className="text-2xl font-bold tracking-tight">SAPIENS</span>
</div>
<div className="flex items-center space-x-4">
<Button variant="ghost" onClick={() => window.history.back()}>
Back
</Button>
<Button variant="outline" onClick={() => window.location.href = "/"}>
Home
</Button>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-6 py-8">
{/* Outlet Header */}
<div className="mb-8">
<div className="flex items-start space-x-6 mb-6">
{outlet.imageUrl ? (
<img
src={outlet.imageUrl}
alt={outlet.name}
className="w-20 h-20 rounded-full object-cover"
/>
) : (
<div className="w-20 h-20 bg-primary/20 rounded-full flex items-center justify-center">
<span className="text-primary font-bold text-2xl">
{outlet.name.charAt(0)}
</span>
</div>
)}
<div className="flex-1">
<h1 className="text-3xl font-bold mb-2">{outlet.name}</h1>
<p className="text-lg text-muted-foreground mb-4">{outlet.description}</p>
<div className="flex items-center space-x-2 mb-4">
<Badge variant="secondary" className="capitalize">
{outlet.category}
</Badge>
{outlet.tags?.map((tag) => (
<Badge key={tag} variant="outline">
{tag}
</Badge>
))}
</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>
{/* Articles Section */}
<div className="mb-8">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">Latest Articles</h2>
<div className="flex items-center space-x-2">
<Button
variant={viewMode === "grid" ? "default" : "outline"}
size="sm"
onClick={() => setViewMode("grid")}
data-testid="button-grid-view"
>
<i className="fas fa-th mr-2"></i>
Grid
</Button>
<Button
variant={viewMode === "list" ? "default" : "outline"}
size="sm"
onClick={() => setViewMode("list")}
data-testid="button-list-view"
>
<i className="fas fa-list mr-2"></i>
List
</Button>
</div>
</div>
{articlesLoading ? (
<div className={`grid gap-6 ${viewMode === "grid" ? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3" : "grid-cols-1"}`}>
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="bg-card border border-border rounded-xl p-6 animate-pulse">
<div className="h-40 bg-muted rounded mb-4"></div>
<div className="h-6 bg-muted rounded mb-2"></div>
<div className="h-4 bg-muted rounded w-2/3"></div>
</div>
))}
</div>
) : articles.length > 0 ? (
<div className={`grid gap-6 ${viewMode === "grid" ? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3" : "grid-cols-1"}`}>
{articles.map((article) => (
<ArticleCard
key={article.id}
article={article}
outlet={outlet}
viewMode={viewMode}
/>
))}
</div>
) : (
<Card>
<CardContent className="p-12 text-center">
<i className="fas fa-newspaper text-4xl text-muted-foreground mb-4"></i>
<h3 className="text-xl font-semibold mb-2">No Articles Yet</h3>
<p className="text-muted-foreground">
This media outlet doesn't have any published articles yet. Check back later!
</p>
</CardContent>
</Card>
)}
</div>
</main>
</div>
);
}