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:
kimjaehyeon0101
2025-09-29 18:42:29 +00:00
parent 44b00ea7ea
commit 6d81e0281a
5 changed files with 67 additions and 15 deletions

View File

@ -22,10 +22,6 @@ externalPort = 3001
localPort = 43349 localPort = 43349
externalPort = 3000 externalPort = 3000
[[ports]]
localPort = 44197
externalPort = 3002
[env] [env]
PORT = "5000" PORT = "5000"

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,8 +1,11 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import type { MediaOutlet } from "@shared/schema"; import type { MediaOutlet } from "@shared/schema";
import { useAuth } from "@/hooks/useAuth"; import { useAuth } from "@/hooks/useAuth";
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
import { useState } from "react";
import { ArrowUpDown } from "lucide-react";
const categories = [ const categories = [
{ id: "people", name: "People", key: "People" }, { id: "people", name: "People", key: "People" },
@ -12,6 +15,7 @@ const categories = [
export default function MainContent() { export default function MainContent() {
const { user } = useAuth(); const { user } = useAuth();
const [sortBy, setSortBy] = useState<"alphabetical" | "traffic">("alphabetical");
const { data: allOutlets = [], isLoading } = useQuery<MediaOutlet[]>({ const { data: allOutlets = [], isLoading } = useQuery<MediaOutlet[]>({
queryKey: ["/api/media-outlets"], 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) => { const getOutletsByCategory = (category: string) => {
return allOutlets.filter(outlet => const filtered = allOutlets.filter(outlet =>
outlet.category.toLowerCase() === category.toLowerCase() 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) => ( const renderOutletCard = (outlet: MediaOutlet) => (
@ -72,6 +85,23 @@ export default function MainContent() {
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 py-4"> <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 ? ( {isLoading ? (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{categories.map((category) => ( {categories.map((category) => (

View File

@ -6,7 +6,8 @@ import { useAuth } from "@/hooks/useAuth";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { isUnauthorizedError } from "@/lib/authUtils"; 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 type { MediaOutlet } from "@shared/schema";
import MediaOutletManagement from "@/components/MediaOutletManagement"; import MediaOutletManagement from "@/components/MediaOutletManagement";
@ -16,6 +17,7 @@ export default function AdminDashboard() {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [selectedOutlet, setSelectedOutlet] = useState<MediaOutlet | null>(null); const [selectedOutlet, setSelectedOutlet] = useState<MediaOutlet | null>(null);
const [managingOutlet, setManagingOutlet] = 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 // Redirect if not authenticated or not admin/superadmin
useEffect(() => { useEffect(() => {
@ -44,11 +46,20 @@ export default function AdminDashboard() {
}, },
}); });
// Filter outlets based on search term // Filter and sort outlets based on search term and sort option
const filteredOutlets = mediaOutlets.filter(outlet => const filteredOutlets = mediaOutlets
.filter(outlet =>
outlet.name.toLowerCase().includes(searchTerm.toLowerCase()) || outlet.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(outlet.description && outlet.description.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 = () => { const handleLogout = () => {
window.location.href = "/api/logout"; window.location.href = "/api/logout";
@ -131,10 +142,24 @@ export default function AdminDashboard() {
</header> </header>
<main className="max-w-7xl mx-auto px-4 py-4"> <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> <h1 className="text-2xl font-bold text-gray-900 mb-2"> </h1>
<p className="text-gray-600"> </p> <p className="text-gray-600"> </p>
</div> </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 ? ( {outletsLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">

View File

@ -47,6 +47,7 @@ export const mediaOutlets = pgTable("media_outlets", {
description: text("description"), description: text("description"),
imageUrl: varchar("image_url"), imageUrl: varchar("image_url"),
tags: text("tags").array(), tags: text("tags").array(),
trafficScore: integer("traffic_score").default(0), // For sorting by traffic
isActive: boolean("is_active").default(true), isActive: boolean("is_active").default(true),
createdAt: timestamp("created_at").defaultNow(), createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(),