Files
sapiens-web3/client/src/pages/MediaOutlet.tsx
kimjaehyeon0101 f04a6da118 Adjust media outlet logo and name alignment for better visual balance
Update MediaOutlet and Report components to reposition the SAPIENS logo and media outlet name. The logo's vertical and horizontal dimensions are adjusted to prevent distortion.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 0fb68265-c270-4198-9584-3d9be9bddb41
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/0fb68265-c270-4198-9584-3d9be9bddb41/sXqrCAq
2025-09-30 06:44:54 +00:00

431 lines
16 KiB
TypeScript

import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useRoute, useLocation } from "wouter";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Search, Settings, User, LogOut, Grid, List, Info } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { useToast } from "@/hooks/use-toast";
import { queryClient } from "@/lib/queryClient";
import ArticleCard from "@/components/ArticleCard";
import Footer from "@/components/Footer";
import LoginModal from "@/components/LoginModal";
import SearchModal from "@/components/SearchModal";
import type { MediaOutlet, Article } from "@shared/schema";
export default function MediaOutlet() {
const [, params] = useRoute("/media/:slug");
const [, setLocation] = useLocation();
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
const [enlargedImage, setEnlargedImage] = useState<string | null>(null);
const { user, isAuthenticated } = useAuth();
const { toast } = useToast();
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 handleLogout = async () => {
try {
const response = await fetch("/api/logout", {
method: "POST",
credentials: "include",
});
if (response.ok) {
toast({
title: "Logout Successful",
description: "You have been successfully logged out.",
});
queryClient.invalidateQueries({ queryKey: ["/api/auth/user"] });
}
} catch (error) {
toast({
title: "Logout Error",
description: "An error occurred during logout.",
variant: "destructive",
});
}
};
const handleAdminPage = () => {
setLocation("/admin");
};
if (outletLoading) {
return (
<div className="flex flex-col min-h-screen bg-gray-50">
{/* Header - Same as Home */}
<header className="bg-white border-b border-gray-200 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-gray-200 rounded-full animate-pulse"></div>
<div className="flex flex-col mt-1">
<div className="h-2.5 w-12 bg-gray-200 rounded animate-pulse mb-0.5"></div>
<div className="h-5 w-24 bg-gray-200 rounded animate-pulse"></div>
</div>
</div>
<div className="flex items-center space-x-4">
<div
className="relative cursor-pointer"
onClick={() => setIsSearchModalOpen(true)}
data-testid="search-container"
>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
<Input
type="text"
placeholder="Search the website"
className="w-80 pl-10 bg-gray-50 border-gray-200 cursor-pointer"
data-testid="input-search"
readOnly
/>
</div>
{isAuthenticated && user ? (
<>
<Button
variant="ghost"
size="sm"
onClick={handleAdminPage}
data-testid="button-admin-dashboard"
>
Admin Dashboard
</Button>
<div className="flex items-center space-x-2 px-3 py-1 bg-gray-100 rounded-md">
<User className="h-4 w-4 text-gray-600" />
<span className="text-sm font-medium text-gray-700" data-testid="user-name">
{user.firstName} {user.lastName}
</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={handleLogout}
data-testid="button-logout"
>
<LogOut className="h-4 w-4" />
</Button>
</>
) : (
<>
<Button
variant="ghost"
size="sm"
onClick={() => setLocation("/auctions")}
data-testid="button-auctions"
>
Auctions
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setIsLoginModalOpen(true)}
data-testid="button-login"
>
Login
</Button>
</>
)}
<Button
variant="ghost"
size="sm"
data-testid="button-settings"
>
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</header>
<div className="flex-1 max-w-7xl mx-auto px-6 py-4 w-full">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/3 mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-2/3 mb-4"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-64 bg-gray-200 rounded-xl"></div>
))}
</div>
</div>
</div>
<Footer />
</div>
);
}
if (!outlet) {
return (
<div className="min-h-screen bg-gray-50 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-gray-600 mb-4">The media outlet you're looking for doesn't exist.</p>
<Button onClick={() => setLocation("/")}>Go to Homepage</Button>
</div>
</div>
);
}
return (
<div className="flex flex-col min-h-screen bg-gray-50">
{/* Header - Same as Home */}
<header className="bg-white border-b border-gray-200 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">
{outlet.imageUrl ? (
<img
src={outlet.imageUrl}
alt={outlet.name}
className="w-10 h-10 rounded-full object-cover cursor-pointer hover:ring-2 hover:ring-blue-400 transition-all"
onClick={() => setEnlargedImage(outlet.imageUrl!)}
data-testid="image-outlet-header-profile"
/>
) : (
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
<span className="text-gray-600 font-bold text-sm">
{outlet.name.charAt(0)}
</span>
</div>
)}
<div className="flex flex-col cursor-pointer hover:opacity-80 transition-opacity mt-1" onClick={() => setLocation("/")}>
<img
src="/attached_assets/logo_black_1759162717640.png"
alt="SAPIENS"
className="h-2.5 w-auto max-w-[45px] mb-0.5"
data-testid="logo-sapiens"
/>
<div className="flex items-center space-x-1.5">
<span className="text-lg font-bold text-gray-900" data-testid="text-outlet-name-header">
{outlet.name}
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center"
aria-label="View outlet information"
>
<Info
className="h-4 w-4 text-gray-500 cursor-pointer hover:text-gray-700"
data-testid="icon-info-header"
/>
</button>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">{outlet.description || "Media Outlet"}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
<div className="flex items-center space-x-4">
<div
className="relative cursor-pointer"
onClick={() => setIsSearchModalOpen(true)}
data-testid="search-container"
>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
<Input
type="text"
placeholder="Search the website"
className="w-80 pl-10 bg-gray-50 border-gray-200 cursor-pointer"
data-testid="input-search"
readOnly
onClick={() => setIsSearchModalOpen(true)}
/>
</div>
{isAuthenticated && user ? (
<>
<Button
variant="ghost"
size="sm"
onClick={() => setLocation("/auctions")}
data-testid="button-auctions"
>
Auctions
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleAdminPage}
data-testid="button-admin-dashboard"
>
Admin Dashboard
</Button>
<div className="flex items-center space-x-2 px-3 py-1 bg-gray-100 rounded-md">
<User className="h-4 w-4 text-gray-600" />
<span className="text-sm font-medium text-gray-700" data-testid="user-name">
{user.firstName} {user.lastName}
</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={handleLogout}
data-testid="button-logout"
>
<LogOut className="h-4 w-4" />
</Button>
</>
) : (
<Button
variant="ghost"
size="sm"
onClick={() => setIsLoginModalOpen(true)}
data-testid="button-login"
>
Login
</Button>
)}
<Button
variant="ghost"
size="sm"
data-testid="button-settings"
>
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</header>
<main className="flex-1 max-w-7xl mx-auto px-6 py-4 pb-32 w-full">
{/* Articles Section */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-xl font-bold">Latest Articles</h2>
<div className="flex items-center space-x-2" role="group" aria-label="Article view mode">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() => setViewMode("grid")}
className={viewMode === "grid" ? "bg-gray-600 text-white hover:bg-gray-700" : ""}
aria-label="Grid view"
aria-pressed={viewMode === "grid"}
data-testid="button-grid-view"
>
<Grid className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Grid view</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() => setViewMode("list")}
className={viewMode === "list" ? "bg-gray-600 text-white hover:bg-gray-700" : ""}
aria-label="List view"
aria-pressed={viewMode === "list"}
data-testid="button-list-view"
>
<List className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>List view</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
{articlesLoading ? (
<div className={`grid gap-4 ${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-white border border-gray-200 rounded-xl p-6 animate-pulse">
<div className="h-40 bg-gray-200 rounded mb-4"></div>
<div className="h-6 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-2/3"></div>
</div>
))}
</div>
) : articles.length > 0 ? (
<div className={`grid gap-4 ${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">
<div className="text-4xl text-gray-400 mb-4">📰</div>
<h3 className="text-xl font-semibold mb-2">No Articles Yet</h3>
<p className="text-gray-600">
This media outlet doesn't have any published articles yet. Check back later!
</p>
</CardContent>
</Card>
)}
</div>
</main>
<Footer />
{/* Login Modal */}
<LoginModal
isOpen={isLoginModalOpen}
onClose={() => setIsLoginModalOpen(false)}
/>
{/* Search Modal */}
<SearchModal
isOpen={isSearchModalOpen}
onClose={() => setIsSearchModalOpen(false)}
/>
{/* Image Enlargement Dialog */}
<Dialog open={!!enlargedImage} onOpenChange={() => setEnlargedImage(null)}>
<DialogContent className="max-w-3xl p-0 bg-transparent border-none">
{enlargedImage && (
<img
src={enlargedImage}
alt="Enlarged profile"
className="w-full h-auto rounded-lg"
data-testid="dialog-enlarged-outlet-image"
/>
)}
</DialogContent>
</Dialog>
</div>
);
}