diff --git a/.replit b/.replit index b0526f5..989f615 100644 --- a/.replit +++ b/.replit @@ -22,6 +22,10 @@ externalPort = 3002 localPort = 37531 externalPort = 3001 +[[ports]] +localPort = 42459 +externalPort = 3003 + [[ports]] localPort = 43349 externalPort = 3000 diff --git a/attached_assets/스크린샷 2025-09-30 오전 4.48.55_1759175355993.png b/attached_assets/스크린샷 2025-09-30 오전 4.48.55_1759175355993.png new file mode 100644 index 0000000..2c5544a Binary files /dev/null and b/attached_assets/스크린샷 2025-09-30 오전 4.48.55_1759175355993.png differ diff --git a/client/src/components/SearchModal.tsx b/client/src/components/SearchModal.tsx new file mode 100644 index 0000000..f37ab59 --- /dev/null +++ b/client/src/components/SearchModal.tsx @@ -0,0 +1,220 @@ +import { useState, useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Search, X } from "lucide-react"; +import { Link } from "wouter"; +import type { MediaOutlet, Article } from "@shared/schema"; + +interface SearchModalProps { + isOpen: boolean; + onClose: () => void; +} + +interface SearchResults { + outlets: MediaOutlet[]; + articles: Article[]; +} + +export default function SearchModal({ isOpen, onClose }: SearchModalProps) { + const [searchQuery, setSearchQuery] = useState(""); + const [debouncedQuery, setDebouncedQuery] = useState(""); + + console.log("SearchModal rendered with isOpen:", isOpen); + + // Debounce search query + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedQuery(searchQuery); + }, 300); + + return () => clearTimeout(timer); + }, [searchQuery]); + + const { data: searchResults, isLoading } = useQuery({ + queryKey: ["/api/search", debouncedQuery], + queryFn: async () => { + if (!debouncedQuery.trim()) { + return { outlets: [], articles: [] }; + } + const res = await fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}`); + if (!res.ok) { + throw new Error(`${res.status}: ${res.statusText}`); + } + return res.json(); + }, + enabled: !!debouncedQuery.trim(), + }); + + const totalResults = (searchResults?.outlets.length || 0) + (searchResults?.articles.length || 0); + + const handleClose = () => { + setSearchQuery(""); + setDebouncedQuery(""); + onClose(); + }; + + const handleLinkClick = () => { + handleClose(); + }; + + return ( + + + Search Articles and Media Outlets + {/* Search Header */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 pr-4 border-2 border-blue-200 focus:border-blue-400" + data-testid="search-modal-input" + autoFocus + /> +
+ +
+ + {/* Search Results */} +
+ {!debouncedQuery.trim() ? ( + /* Empty State */ +
+

Start typing to search articles and outlets

+
+ ) : isLoading ? ( + /* Loading State */ +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+
+
+ ))} +
+ ) : totalResults === 0 ? ( + /* No Results */ +
+

No results found for "{debouncedQuery}"

+

Try a different search term

+
+ ) : ( + /* Search Results */ + <> +
+ {totalResults} results found +
+ + {/* Outlets Section */} + {searchResults && searchResults.outlets.length > 0 && ( +
+

+ Outlets ({searchResults.outlets.length}) +

+
+ {searchResults.outlets.map((outlet) => ( + +
+
+ {outlet.imageUrl ? ( + {outlet.name} + ) : ( + + {outlet.name.charAt(0)} + + )} +
+
+
+

+ {outlet.name} +

+ + {outlet.category} + +
+

+ {outlet.description || "Media Outlet"} +

+
+
+ + ))} +
+
+ )} + + {/* Articles Section */} + {searchResults && searchResults.articles.length > 0 && ( +
+

+ Articles ({searchResults.articles.length}) +

+
+ {searchResults.articles.map((article) => ( + +
+ {article.imageUrl && ( +
+ {article.title} +
+ )} +
+

+ {article.title} +

+

+ {article.excerpt || ""} +

+ {article.publishedAt && ( +

+ {new Date(article.publishedAt).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + })} +

+ )} +
+
+ + ))} +
+
+ )} + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 47e7ea8..1022a83 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -5,12 +5,14 @@ import { Search, Settings, User, LogOut } from "lucide-react"; import { useState } from "react"; import MainContent from "@/components/MainContent"; import LoginModal from "@/components/LoginModal"; +import SearchModal from "@/components/SearchModal"; import { useToast } from "@/hooks/use-toast"; import { queryClient } from "@/lib/queryClient"; export default function Home() { const { user, isAuthenticated } = useAuth(); const [isLoginModalOpen, setIsLoginModalOpen] = useState(false); + const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); const { toast } = useToast(); const handleLogout = async () => { @@ -58,13 +60,25 @@ export default function Home() {
-
- +
{ + console.log("Search div clicked, opening modal..."); + setIsSearchModalOpen(true); + }} + data-testid="search-container" + > + { + console.log("Search input clicked, opening modal..."); + setIsSearchModalOpen(true); + }} />
@@ -126,6 +140,12 @@ export default function Home() { isOpen={isLoginModalOpen} onClose={() => setIsLoginModalOpen(false)} /> + + {/* Search Modal */} + setIsSearchModalOpen(false)} + />
); } \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index 60aa740..fa76b4c 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -354,6 +354,23 @@ export async function registerRoutes(app: Express): Promise { } }); + // Search routes + app.get('/api/search', async (req, res) => { + try { + const { q } = req.query; + + if (!q || typeof q !== 'string' || q.trim().length === 0) { + return res.json({ outlets: [], articles: [] }); + } + + const results = await storage.search(q.trim()); + res.json(results); + } catch (error) { + console.error("Error performing search:", error); + res.status(500).json({ message: "Failed to perform search" }); + } + }); + const httpServer = createServer(app); return httpServer; } diff --git a/server/storage.ts b/server/storage.ts index d81e4a3..2af5991 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -82,6 +82,12 @@ export interface IStorage { liveAuctions: number; totalRevenue: number; }>; + + // Search operations + search(query: string): Promise<{ + outlets: MediaOutlet[]; + articles: Article[]; + }>; } export class DatabaseStorage implements IStorage { @@ -381,6 +387,44 @@ export class DatabaseStorage implements IStorage { totalRevenue: revenueSum.sum }; } + + // Search operations + async search(query: string): Promise<{ + outlets: MediaOutlet[]; + articles: Article[]; + }> { + const searchTerm = query.toLowerCase(); + + // Search media outlets + const foundOutlets = await db + .select() + .from(mediaOutlets) + .where(eq(mediaOutlets.isActive, true)) + .limit(50); + + const filteredOutlets = foundOutlets.filter(outlet => + outlet.name.toLowerCase().includes(searchTerm) || + (outlet.description && outlet.description.toLowerCase().includes(searchTerm)) + ).slice(0, 10); + + // Search articles + const foundArticles = await db + .select() + .from(articles) + .orderBy(desc(articles.publishedAt)) + .limit(100); + + const filteredArticles = foundArticles.filter(article => + article.title.toLowerCase().includes(searchTerm) || + (article.content && article.content.toLowerCase().includes(searchTerm)) || + (article.excerpt && article.excerpt.toLowerCase().includes(searchTerm)) + ).slice(0, 20); + + return { + outlets: filteredOutlets, + articles: filteredArticles + }; + } } export const storage = new DatabaseStorage();