Files
sapiens-web/client/src/pages/MediaOutlet.tsx
kimjaehyeon0101 ddb99c5218 Update the media outlet page to match the overall site design
Refactors the UI and CSS of the auction section and article view toggles to align with the site's existing visual theme, changing accent colors from orange to gray and updating button styles.

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/UXwUwik
2025-09-30 02:23:08 +00:00

590 lines
22 KiB
TypeScript

import { useState } from "react";
import { useQuery, useMutation } 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 { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Gavel, Clock, TrendingUp, Search, Settings, User, LogOut, Grid, List, Edit } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { useToast } from "@/hooks/use-toast";
import { queryClient, apiRequest } 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, Auction } 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 [isEditDescModalOpen, setIsEditDescModalOpen] = useState(false);
const [newDescription, setNewDescription] = useState("");
const [alternativeDescription, setAlternativeDescription] = useState("Partner at Type3 Capital and Non-Executive Director at TrueFi DAO with a strong background in fund management, venture capital, and digital assets");
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 { data: auction, isLoading: auctionLoading } = useQuery<Auction>({
queryKey: ["/api/media-outlets", params?.slug, "auction"],
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");
};
const updateDescriptionMutation = useMutation({
mutationFn: async (description: string) => {
await apiRequest("PATCH", `/api/media-outlets/${params?.slug}`, { description });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/media-outlets", params?.slug] });
toast({
title: "Success",
description: "Description updated successfully",
});
setIsEditDescModalOpen(false);
},
onError: () => {
toast({
title: "Error",
description: "Failed to update description",
variant: "destructive",
});
},
});
const handleEditDescription = () => {
setNewDescription(outlet?.description || "");
setIsEditDescModalOpen(true);
};
const handleSaveDescription = (useAlternative: boolean = false) => {
const descToSave = useAlternative ? alternativeDescription : newDescription;
updateDescriptionMutation.mutate(descToSave);
};
const formatCurrency = (amount: string | null) => {
if (!amount) return "$0";
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(parseFloat(amount));
};
const formatTimeRemaining = (endDate: Date | string) => {
const end = new Date(endDate);
const now = new Date();
const diff = end.getTime() - now.getTime();
if (diff <= 0) return "Auction Ended";
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
if (days > 0) return `${days}d ${hours}h remaining`;
if (hours > 0) return `${hours}h remaining`;
return "Less than 1h remaining";
};
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">
<img
src="/attached_assets/logo_black_1759162717640.png"
alt="SAPIENS"
className="h-6 w-auto"
data-testid="logo-sapiens"
/>
</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">
<img
src="/attached_assets/logo_black_1759162717640.png"
alt="SAPIENS"
className="h-6 w-auto cursor-pointer"
data-testid="logo-sapiens"
onClick={() => setLocation("/")}
/>
</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 w-full">
{/* Outlet Header */}
<div className="mb-4">
<div className="flex items-start space-x-4 mb-3">
{outlet.imageUrl ? (
<img
src={outlet.imageUrl}
alt={outlet.name}
className="w-16 h-16 rounded-full object-cover"
/>
) : (
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center">
<span className="text-gray-600 font-bold text-xl">
{outlet.name.charAt(0)}
</span>
</div>
)}
<div className="flex-1">
<div className="flex items-center space-x-2 mb-2">
<h1 className="text-2xl font-bold">{outlet.name}</h1>
<Badge variant="secondary" className="capitalize">
{outlet.category}
</Badge>
</div>
<div className="flex items-center space-x-2 mb-2">
<p className="text-gray-600">{outlet.description}</p>
{user && (user.role === 'admin' || user.role === 'superadmin') && (
<Button
variant="ghost"
size="sm"
onClick={handleEditDescription}
data-testid="button-edit-description"
>
<Edit className="h-3 w-3" />
</Button>
)}
</div>
{outlet.tags && outlet.tags.length > 0 && (
<div className="flex items-center space-x-2 mb-2">
{outlet.tags.map((tag) => (
<Badge key={tag} variant="outline">
{tag}
</Badge>
))}
</div>
)}
{/* Auction Section */}
{!auctionLoading && auction ? (
<Card className="border-gray-200 bg-gray-50">
<CardContent className="p-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Gavel className="h-5 w-5 text-gray-600" />
<span className="font-semibold text-gray-900">Management Rights Auction</span>
</div>
<div className="flex items-center space-x-2 text-sm text-gray-700">
<TrendingUp className="h-4 w-4" />
<span>Current Highest Bid: {formatCurrency(auction.currentBid)}</span>
</div>
<div className="flex items-center space-x-2 text-sm text-gray-700">
<Clock className="h-4 w-4" />
<span>{formatTimeRemaining(auction.endDate)}</span>
</div>
</div>
<Button
onClick={() => setLocation(`/media/${params?.slug}/auction`)}
size="sm"
className="bg-gray-600 hover:bg-gray-700 text-white"
data-testid="button-auction"
>
Place Bid
</Button>
</div>
</CardContent>
</Card>
) : !auctionLoading && !auction ? (
<Card className="border-gray-200 bg-gray-50">
<CardContent className="p-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Gavel className="h-5 w-5 text-gray-500" />
<span className="text-gray-700">No active auction currently</span>
</div>
<Button
variant="outline"
size="sm"
disabled
data-testid="button-no-auction"
>
Auction Pending
</Button>
</div>
</CardContent>
</Card>
) : (
<Card className="border-gray-200 bg-gray-50">
<CardContent className="p-3">
<div className="flex items-center space-x-2">
<div className="animate-pulse flex space-x-4">
<div className="rounded-full bg-gray-300 h-5 w-5"></div>
<div className="h-4 bg-gray-300 rounded w-24"></div>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</div>
</div>
{/* 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">
<Button
variant="outline"
size="sm"
onClick={() => setViewMode("grid")}
className={viewMode === "grid" ? "bg-gray-600 text-white hover:bg-gray-700" : ""}
data-testid="button-grid-view"
>
<Grid className="h-4 w-4 mr-2" />
Grid
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setViewMode("list")}
className={viewMode === "list" ? "bg-gray-600 text-white hover:bg-gray-700" : ""}
data-testid="button-list-view"
>
<List className="h-4 w-4 mr-2" />
List
</Button>
</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)}
/>
{/* Edit Description Modal */}
<Dialog open={isEditDescModalOpen} onOpenChange={setIsEditDescModalOpen}>
<DialogContent data-testid="modal-edit-description">
<DialogHeader>
<DialogTitle>Edit Description</DialogTitle>
<DialogDescription>
Update the description for {outlet?.name}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Current Description</label>
<Textarea
value={newDescription}
onChange={(e) => setNewDescription(e.target.value)}
rows={3}
className="mt-1"
data-testid="textarea-description"
/>
</div>
{outlet?.slug === 'krysh-parker' && (
<div className="p-3 bg-blue-50 rounded-lg border border-blue-200">
<p className="text-sm font-medium text-blue-900 mb-2">Alternative Description</p>
<p className="text-sm text-blue-700 mb-3">{alternativeDescription}</p>
<Button
variant="outline"
size="sm"
onClick={() => handleSaveDescription(true)}
disabled={updateDescriptionMutation.isPending}
data-testid="button-use-alternative"
>
Use This Description
</Button>
</div>
)}
<div className="flex space-x-2">
<Button
onClick={() => handleSaveDescription(false)}
disabled={updateDescriptionMutation.isPending}
data-testid="button-save-description"
>
{updateDescriptionMutation.isPending ? "Saving..." : "Save Changes"}
</Button>
<Button
variant="outline"
onClick={() => setIsEditDescModalOpen(false)}
data-testid="button-cancel-edit"
>
Cancel
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
}