Add sorting options for media outlets by name or traffic
Introduce alphabetical and traffic score sorting for media outlets in both the main content and admin dashboard views, updating the schema to include a trafficScore field. 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/jvFIdY3
This commit is contained in:
4
.replit
4
.replit
@ -22,10 +22,6 @@ externalPort = 3001
|
||||
localPort = 43349
|
||||
externalPort = 3000
|
||||
|
||||
[[ports]]
|
||||
localPort = 44197
|
||||
externalPort = 3002
|
||||
|
||||
[env]
|
||||
PORT = "5000"
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@ -1,8 +1,11 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import type { MediaOutlet } from "@shared/schema";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import Footer from "@/components/Footer";
|
||||
import { useState } from "react";
|
||||
import { ArrowUpDown } from "lucide-react";
|
||||
|
||||
const categories = [
|
||||
{ id: "people", name: "People", key: "People" },
|
||||
@ -12,6 +15,7 @@ const categories = [
|
||||
|
||||
export default function MainContent() {
|
||||
const { user } = useAuth();
|
||||
const [sortBy, setSortBy] = useState<"alphabetical" | "traffic">("alphabetical");
|
||||
|
||||
const { data: allOutlets = [], isLoading } = useQuery<MediaOutlet[]>({
|
||||
queryKey: ["/api/media-outlets"],
|
||||
@ -26,11 +30,20 @@ export default function MainContent() {
|
||||
},
|
||||
});
|
||||
|
||||
// Group outlets by category
|
||||
// Group outlets by category and sort
|
||||
const getOutletsByCategory = (category: string) => {
|
||||
return allOutlets.filter(outlet =>
|
||||
const filtered = allOutlets.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 renderOutletCard = (outlet: MediaOutlet) => (
|
||||
@ -72,6 +85,23 @@ export default function MainContent() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||
{/* Sorting Controls */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">언론매체</h1>
|
||||
<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="sort-select">
|
||||
<SelectValue placeholder="정렬 방식" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="alphabetical" data-testid="sort-alphabetical">ABC 순</SelectItem>
|
||||
<SelectItem value="traffic" data-testid="sort-traffic">트래픽 순</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{categories.map((category) => (
|
||||
|
||||
@ -6,7 +6,8 @@ import { useAuth } from "@/hooks/useAuth";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { isUnauthorizedError } from "@/lib/authUtils";
|
||||
import { Search, Settings } from "lucide-react";
|
||||
import { Search, Settings, ArrowUpDown } from "lucide-react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import type { MediaOutlet } from "@shared/schema";
|
||||
import MediaOutletManagement from "@/components/MediaOutletManagement";
|
||||
|
||||
@ -16,6 +17,7 @@ export default function AdminDashboard() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedOutlet, setSelectedOutlet] = useState<MediaOutlet | null>(null);
|
||||
const [managingOutlet, setManagingOutlet] = useState<MediaOutlet | null>(null);
|
||||
const [sortBy, setSortBy] = useState<"alphabetical" | "traffic">("alphabetical");
|
||||
|
||||
// Redirect if not authenticated or not admin/superadmin
|
||||
useEffect(() => {
|
||||
@ -44,11 +46,20 @@ export default function AdminDashboard() {
|
||||
},
|
||||
});
|
||||
|
||||
// Filter outlets based on search term
|
||||
const filteredOutlets = mediaOutlets.filter(outlet =>
|
||||
// Filter and sort outlets based on search term and sort option
|
||||
const filteredOutlets = mediaOutlets
|
||||
.filter(outlet =>
|
||||
outlet.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(outlet.description && outlet.description.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
);
|
||||
)
|
||||
.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";
|
||||
@ -131,10 +142,24 @@ export default function AdminDashboard() {
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 py-4">
|
||||
<div className="mb-6">
|
||||
<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>
|
||||
</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="정렬 방식" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="alphabetical" data-testid="admin-sort-alphabetical">ABC 순</SelectItem>
|
||||
<SelectItem value="traffic" data-testid="admin-sort-traffic">트래픽 순</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{outletsLoading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
|
||||
@ -47,6 +47,7 @@ export const mediaOutlets = pgTable("media_outlets", {
|
||||
description: text("description"),
|
||||
imageUrl: varchar("image_url"),
|
||||
tags: text("tags").array(),
|
||||
trafficScore: integer("traffic_score").default(0), // For sorting by traffic
|
||||
isActive: boolean("is_active").default(true),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
|
||||
Reference in New Issue
Block a user