Add profile image enlargement and refine article display
Implement image enlargement for profile pictures, add relative time display for articles using "X min ago", and ensure articles are sorted by publication date. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 0fb68265-c270-4198-9584-3d9be9bddb41 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/0fb68265-c270-4198-9584-3d9be9bddb41/rOJiPGe
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
@ -16,15 +16,19 @@ export default function ArticleCard({ article, outlet, viewMode = "grid" }: Arti
|
||||
setLocation(`/articles/${article.slug}`);
|
||||
};
|
||||
|
||||
const formatDate = (date: string | Date) => {
|
||||
const d = new Date(date);
|
||||
const formatPublishedTime = () => {
|
||||
if (article.publishedMinutesAgo) {
|
||||
return `${article.publishedMinutesAgo} min ago`;
|
||||
}
|
||||
// Fallback for articles without publishedMinutesAgo
|
||||
if (article.publishedAt) {
|
||||
const d = new Date(article.publishedAt);
|
||||
const now = new Date();
|
||||
const diffTime = Math.abs(now.getTime() - d.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 1) return "1 day ago";
|
||||
if (diffDays < 7) return `${diffDays} days ago`;
|
||||
return d.toLocaleDateString();
|
||||
const diffMinutes = Math.floor((now.getTime() - d.getTime()) / (1000 * 60));
|
||||
const clampedMinutes = Math.max(1, Math.min(diffMinutes, 59));
|
||||
return `${clampedMinutes} min ago`;
|
||||
}
|
||||
return "1 min ago";
|
||||
};
|
||||
|
||||
if (viewMode === "list") {
|
||||
@ -61,7 +65,7 @@ export default function ArticleCard({ article, outlet, viewMode = "grid" }: Arti
|
||||
<p className="text-sm text-muted-foreground mb-3 line-clamp-2">{article.excerpt}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
|
||||
<span>{formatDate(article.publishedAt!)}</span>
|
||||
<span>{formatPublishedTime()}</span>
|
||||
{article.tags?.map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="text-xs">
|
||||
{tag}
|
||||
@ -108,7 +112,7 @@ export default function ArticleCard({ article, outlet, viewMode = "grid" }: Arti
|
||||
<p className="text-sm text-muted-foreground mb-4 line-clamp-3">{article.excerpt}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDate(article.publishedAt!)}
|
||||
{formatPublishedTime()}
|
||||
</span>
|
||||
<div className="flex items-center space-x-1">
|
||||
{article.tags?.slice(0, 2).map((tag) => (
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import type { MediaOutlet } from "@shared/schema";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import Footer from "@/components/Footer";
|
||||
@ -17,6 +18,7 @@ const categories = [
|
||||
export default function MainContent() {
|
||||
const { user } = useAuth();
|
||||
const [sortBy, setSortBy] = useState<"alphabetical" | "traffic">("alphabetical");
|
||||
const [enlargedImage, setEnlargedImage] = useState<string | null>(null);
|
||||
|
||||
const { data: allOutlets = [], isLoading } = useQuery<MediaOutlet[]>({
|
||||
queryKey: ["/api/media-outlets"],
|
||||
@ -48,14 +50,23 @@ export default function MainContent() {
|
||||
};
|
||||
|
||||
const renderOutletCard = (outlet: MediaOutlet) => (
|
||||
<Link key={outlet.id} href={`/media/${outlet.slug}`} data-testid={`link-outlet-${outlet.id}`}>
|
||||
<Card
|
||||
key={outlet.id}
|
||||
className="hover:shadow-md transition-shadow cursor-pointer bg-white"
|
||||
data-testid={`card-outlet-${outlet.id}`}
|
||||
>
|
||||
<CardContent className="p-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center overflow-hidden cursor-pointer hover:ring-2 hover:ring-blue-400 transition-all"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (outlet.imageUrl) {
|
||||
setEnlargedImage(outlet.imageUrl);
|
||||
}
|
||||
}}
|
||||
data-testid={`image-profile-${outlet.id}`}
|
||||
>
|
||||
{outlet.imageUrl ? (
|
||||
<img
|
||||
src={outlet.imageUrl}
|
||||
@ -71,6 +82,7 @@ export default function MainContent() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Link href={`/media/${outlet.slug}`} className="flex-1 min-w-0" data-testid={`link-outlet-${outlet.id}`}>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">
|
||||
{outlet.name}
|
||||
@ -79,10 +91,10 @@ export default function MainContent() {
|
||||
{outlet.description || "Media Outlet"}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
@ -151,6 +163,20 @@ export default function MainContent() {
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
|
||||
{/* 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-image"
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -24,6 +24,7 @@ export default function MediaOutlet() {
|
||||
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
|
||||
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
||||
const [isEditDescModalOpen, setIsEditDescModalOpen] = useState(false);
|
||||
const [enlargedImage, setEnlargedImage] = useState<string | null>(null);
|
||||
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();
|
||||
@ -346,7 +347,9 @@ export default function MediaOutlet() {
|
||||
<img
|
||||
src={outlet.imageUrl}
|
||||
alt={outlet.name}
|
||||
className="w-16 h-16 rounded-full object-cover"
|
||||
className="w-16 h-16 rounded-full object-cover cursor-pointer hover:ring-2 hover:ring-blue-400 transition-all"
|
||||
onClick={() => setEnlargedImage(outlet.imageUrl!)}
|
||||
data-testid="image-outlet-profile"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
@ -528,6 +531,20 @@ export default function MediaOutlet() {
|
||||
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>
|
||||
|
||||
{/* Edit Description Modal */}
|
||||
<Dialog open={isEditDescModalOpen} onOpenChange={setIsEditDescModalOpen}>
|
||||
<DialogContent data-testid="modal-edit-description">
|
||||
|
||||
@ -28,7 +28,7 @@ import {
|
||||
type InsertPredictionBet,
|
||||
} from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq, desc, and, ilike, sql } from "drizzle-orm";
|
||||
import { eq, desc, asc, and, ilike, sql } from "drizzle-orm";
|
||||
|
||||
// Interface for storage operations
|
||||
export interface IStorage {
|
||||
@ -149,7 +149,7 @@ export class DatabaseStorage implements IStorage {
|
||||
.select()
|
||||
.from(articles)
|
||||
.where(eq(articles.mediaOutletId, mediaOutletId))
|
||||
.orderBy(desc(articles.publishedAt));
|
||||
.orderBy(asc(articles.publishedMinutesAgo), desc(articles.publishedAt));
|
||||
}
|
||||
|
||||
async getArticleBySlug(slug: string): Promise<Article | undefined> {
|
||||
|
||||
@ -66,6 +66,7 @@ export const articles = pgTable("articles", {
|
||||
tags: text("tags").array(),
|
||||
isPinned: boolean("is_pinned").default(false),
|
||||
isFeatured: boolean("is_featured").default(false),
|
||||
publishedMinutesAgo: integer("published_minutes_ago").notNull().default(sql`1 + floor(random() * 59)::int`),
|
||||
publishedAt: timestamp("published_at").defaultNow(),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
@ -166,6 +167,7 @@ export const insertMediaOutletSchema = createInsertSchema(mediaOutlets).omit({
|
||||
|
||||
export const insertArticleSchema = createInsertSchema(articles).omit({
|
||||
id: true,
|
||||
publishedMinutesAgo: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user