Translate admin dashboard and media outlet management to English
Refactors the admin dashboard to display content in English, adds functionality to sort media outlets by alphabetical order or traffic score, and adjusts the UI to remove individual media outlet chips, preparing for a three-column layout for people, topics, and companies. 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/YptCfK0
This commit is contained in:
4
.replit
4
.replit
@ -22,10 +22,6 @@ externalPort = 3002
|
||||
localPort = 37531
|
||||
externalPort = 3001
|
||||
|
||||
[[ports]]
|
||||
localPort = 39291
|
||||
externalPort = 3003
|
||||
|
||||
[[ports]]
|
||||
localPort = 43349
|
||||
externalPort = 3000
|
||||
|
||||
@ -61,6 +61,22 @@ export default function AdminDashboard() {
|
||||
}
|
||||
});
|
||||
|
||||
// Group outlets by category and sort
|
||||
const getOutletsByCategory = (category: string) => {
|
||||
const filtered = filteredOutlets.filter(outlet =>
|
||||
outlet.category.toLowerCase() === category.toLowerCase()
|
||||
);
|
||||
|
||||
return filtered.sort((a, b) => {
|
||||
if (sortBy === "alphabetical") {
|
||||
return a.name.localeCompare(b.name);
|
||||
} else {
|
||||
// Sort by traffic score (descending - highest traffic first)
|
||||
return (b.trafficScore || 0) - (a.trafficScore || 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
window.location.href = "/api/logout";
|
||||
};
|
||||
@ -117,7 +133,7 @@ export default function AdminDashboard() {
|
||||
onClick={() => window.location.href = "/"}
|
||||
data-testid="button-home"
|
||||
>
|
||||
홈페이지
|
||||
Home
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@ -144,18 +160,18 @@ export default function AdminDashboard() {
|
||||
<main className="max-w-7xl mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">관리자 대시보드</h1>
|
||||
<p className="text-gray-600">관리할 언론매체를 검색하고 선택하세요</p>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">Admin Dashboard</h1>
|
||||
<p className="text-gray-600">Search and select media outlets to manage</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ArrowUpDown className="h-4 w-4 text-gray-500" />
|
||||
<Select value={sortBy} onValueChange={(value: "alphabetical" | "traffic") => setSortBy(value)}>
|
||||
<SelectTrigger className="w-[180px]" data-testid="admin-sort-select">
|
||||
<SelectValue placeholder="정렬 방식" />
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="alphabetical" data-testid="admin-sort-alphabetical">ABC 순</SelectItem>
|
||||
<SelectItem value="traffic" data-testid="admin-sort-traffic">트래픽 순</SelectItem>
|
||||
<SelectItem value="alphabetical" data-testid="admin-sort-alphabetical">Alphabetical</SelectItem>
|
||||
<SelectItem value="traffic" data-testid="admin-sort-traffic">Traffic</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@ -180,20 +196,27 @@ export default function AdminDashboard() {
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4 text-sm text-gray-500">
|
||||
{filteredOutlets.length}개의 언론매체 {searchTerm && `(${searchTerm} 검색 결과)`}
|
||||
{filteredOutlets.length} media outlets {searchTerm && `(search results for "${searchTerm}")`}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredOutlets.map((outlet) => (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* People Section */}
|
||||
<div data-testid="admin-section-people">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-2">
|
||||
People
|
||||
<span className="text-gray-400 text-base ml-2">({getOutletsByCategory("People").length})</span>
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{getOutletsByCategory("People").map((outlet) => (
|
||||
<Card
|
||||
key={outlet.id}
|
||||
className="hover:shadow-md transition-shadow cursor-pointer bg-white"
|
||||
onClick={() => setSelectedOutlet(outlet)}
|
||||
data-testid={`admin-card-outlet-${outlet.id}`}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center overflow-hidden">
|
||||
<CardContent className="p-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center overflow-hidden">
|
||||
{outlet.imageUrl ? (
|
||||
<img
|
||||
src={outlet.imageUrl}
|
||||
@ -202,7 +225,7 @@ export default function AdminDashboard() {
|
||||
style={{objectPosition: 'center top'}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<div className="w-6 h-6 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<span className="text-gray-600 text-sm font-medium">
|
||||
{outlet.name.charAt(0)}
|
||||
</span>
|
||||
@ -216,22 +239,113 @@ export default function AdminDashboard() {
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
{outlet.description || "Media Outlet"}
|
||||
</p>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full">
|
||||
{outlet.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Topics Section */}
|
||||
<div data-testid="admin-section-topics">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-2">
|
||||
Topics
|
||||
<span className="text-gray-400 text-base ml-2">({getOutletsByCategory("Topics").length})</span>
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{getOutletsByCategory("Topics").map((outlet) => (
|
||||
<Card
|
||||
key={outlet.id}
|
||||
className="hover:shadow-md transition-shadow cursor-pointer bg-white"
|
||||
onClick={() => setSelectedOutlet(outlet)}
|
||||
data-testid={`admin-card-outlet-${outlet.id}`}
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center overflow-hidden">
|
||||
{outlet.imageUrl ? (
|
||||
<img
|
||||
src={outlet.imageUrl}
|
||||
alt={outlet.name}
|
||||
className="w-full h-full object-cover"
|
||||
style={{objectPosition: 'center top'}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-6 h-6 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<span className="text-gray-600 text-sm font-medium">
|
||||
{outlet.name.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">
|
||||
{outlet.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
{outlet.description || "Media Outlet"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Companies Section */}
|
||||
<div data-testid="admin-section-companies">
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-2">
|
||||
Companies
|
||||
<span className="text-gray-400 text-base ml-2">({getOutletsByCategory("Companies").length})</span>
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{getOutletsByCategory("Companies").map((outlet) => (
|
||||
<Card
|
||||
key={outlet.id}
|
||||
className="hover:shadow-md transition-shadow cursor-pointer bg-white"
|
||||
onClick={() => setSelectedOutlet(outlet)}
|
||||
data-testid={`admin-card-outlet-${outlet.id}`}
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center overflow-hidden">
|
||||
{outlet.imageUrl ? (
|
||||
<img
|
||||
src={outlet.imageUrl}
|
||||
alt={outlet.name}
|
||||
className="w-full h-full object-cover"
|
||||
style={{objectPosition: 'center top'}}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-6 h-6 bg-gray-300 rounded-full flex items-center justify-center">
|
||||
<span className="text-gray-600 text-sm font-medium">
|
||||
{outlet.name.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">
|
||||
{outlet.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
{outlet.description || "Media Outlet"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filteredOutlets.length === 0 && searchTerm && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-lg mb-2">검색 결과가 없습니다</div>
|
||||
<div className="text-gray-500 text-sm">다른 검색어를 시도해보세요</div>
|
||||
<div className="text-gray-400 text-lg mb-2">No results found</div>
|
||||
<div className="text-gray-500 text-sm">Try a different search term</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@ -272,14 +386,14 @@ export default function AdminDashboard() {
|
||||
}}
|
||||
data-testid="button-manage-outlet"
|
||||
>
|
||||
관리하기
|
||||
Manage
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSelectedOutlet(null)}
|
||||
data-testid="button-cancel"
|
||||
>
|
||||
취소
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user