Compare commits

..

10 Commits

Author SHA1 Message Date
0eee17a43e Add a visual sparkle effect for new news updates on the Erling Haaland media outlet
Introduces a new API endpoint `/api/articles` and implements a client-side animation to visually alert users of new articles on the 'erling-haaland' outlet using CSS keyframes and conditional rendering.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: d9e77062-eeec-4c95-9131-905f69a78072
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/d9e77062-eeec-4c95-9131-905f69a78072/VftcvwB
2025-10-01 06:07:35 +00:00
b5e8cf3d3a Remove all shaking animation features from the platform
Removed the CSS keyframes and animation classes related to shaking animations in client/src/index.css and conditional application of 'animate-shake' class in client/src/components/MainContent.tsx.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: f8b8f0ea-e52e-4118-9b6b-e44184e17203
Replit-Commit-Checkpoint-Type: full_checkpoint
2025-10-01 05:50:35 +00:00
d4d521b40b Remove card vibration animation for specific content
Removed conditional logic for 'animate-shake' and 'animate-float-shake' classes in MainContent.tsx, simplifying animation to only 'animate-float' when articles are present.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: f8b8f0ea-e52e-4118-9b6b-e44184e17203
Replit-Commit-Checkpoint-Type: full_checkpoint
2025-10-01 05:49:07 +00:00
4ff91ab090 Saved your changes before starting work
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: f8b8f0ea-e52e-4118-9b6b-e44184e17203
Replit-Commit-Checkpoint-Type: full_checkpoint
2025-10-01 05:47:57 +00:00
7c5025ee10 Add distinct animations and styling to media outlet cards
Introduce conditional styling and animations to media outlet cards based on article count and specific outlet slugs.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: f8b8f0ea-e52e-4118-9b6b-e44184e17203
Replit-Commit-Checkpoint-Type: full_checkpoint
2025-10-01 05:47:18 +00:00
52ccfc324c Restored to '0894a95fa90f5d79483f1bc98af2ccf244dd36eb'
Replit-Restored-To: 0894a95fa9
2025-10-01 05:45:58 +00:00
730720251a Saved your changes before rolling back
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: f8b8f0ea-e52e-4118-9b6b-e44184e17203
Replit-Commit-Checkpoint-Type: full_checkpoint
2025-10-01 05:45:50 +00:00
bf5ff95c0e Saved your changes before starting work
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: f8b8f0ea-e52e-4118-9b6b-e44184e17203
Replit-Commit-Checkpoint-Type: full_checkpoint
2025-10-01 05:45:47 +00:00
0894a95fa9 Improve the visual appearance of reports with enhanced styling
Update the Report page to apply custom CSS to iframe content for a more aesthetically pleasing presentation.

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/g8mUCzT
2025-09-30 07:54:26 +00:00
7395b0e038 Adjust report page layout to reduce side margins
Update max-width and horizontal padding in Report.tsx for the main content area to better fit the report display.

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/g8mUCzT
2025-09-30 07:48:45 +00:00
6 changed files with 255 additions and 13 deletions

View File

@ -30,6 +30,10 @@ externalPort = 3003
localPort = 43349 localPort = 43349
externalPort = 3000 externalPort = 3000
[[ports]]
localPort = 43777
externalPort = 4200
[env] [env]
PORT = "5000" PORT = "5000"

View File

@ -73,21 +73,48 @@ export default function MainContent() {
const renderOutletCard = (outlet: MediaOutlet) => { const renderOutletCard = (outlet: MediaOutlet) => {
const articleCount = articleCountByOutlet[outlet.id] || 0; const articleCount = articleCountByOutlet[outlet.id] || 0;
const hasArticles = articleCount > 0; const hasArticles = articleCount > 0;
const isErlingHaaland = outlet.slug === 'erling-haaland';
const showSpecialEffect = isErlingHaaland && hasArticles;
const animationClass = hasArticles
? 'animate-float ring-2 ring-blue-400/30 shadow-lg'
: '';
return ( return (
<Card <Card
key={outlet.id} key={outlet.id}
className={`hover:shadow-md transition-all cursor-pointer bg-white relative ${ className={`hover:shadow-md transition-all cursor-pointer bg-white relative ${animationClass}`}
hasArticles ? 'animate-float ring-2 ring-blue-400/30 shadow-lg' : ''
}`}
data-testid={`card-outlet-${outlet.id}`} data-testid={`card-outlet-${outlet.id}`}
> >
{hasArticles && ( {hasArticles && (
<div className="absolute -top-2 -right-2 z-10"> <div className="absolute -top-2 -right-2 z-10">
<Badge className="bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg animate-pulse"> <Badge className={`bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg animate-pulse ${showSpecialEffect ? 'animate-glow-pulse' : ''}`}>
<Sparkles className="h-3 w-3 mr-1" /> <Sparkles className="h-3 w-3 mr-1" />
NEW NEW
</Badge> </Badge>
{showSpecialEffect && (
<div className="absolute inset-0 pointer-events-none" data-testid="sparkle-effect-erling">
{[...Array(6)].map((_, i) => {
const angle = (i * 60) * Math.PI / 180;
const distance = 20 + (i % 2) * 10;
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
const delay = i * 0.2;
return (
<div
key={i}
className="absolute top-1/2 left-1/2 w-2 h-2 rounded-full bg-gradient-to-r from-yellow-400 to-pink-500"
style={{
animation: `sparkle 2s ease-in-out ${delay}s infinite`,
['--sparkle-x' as any]: `${x}px`,
['--sparkle-y' as any]: `${y}px`,
}}
/>
);
})}
</div>
)}
</div> </div>
)} )}
<CardContent className="p-2"> <CardContent className="p-2">

View File

@ -137,10 +137,49 @@
animation: float 3s ease-in-out infinite; animation: float 3s ease-in-out infinite;
} }
@keyframes sparkle {
0% {
opacity: 0;
transform: translate(0, 0) scale(0);
}
50% {
opacity: 1;
}
100% {
opacity: 0;
transform: translate(var(--sparkle-x), var(--sparkle-y)) scale(1);
}
}
@keyframes shimmer {
0% {
background-position: -100% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes glow-pulse {
0%, 100% {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.5), 0 0 40px rgba(168, 85, 247, 0.3);
}
50% {
box-shadow: 0 0 30px rgba(59, 130, 246, 0.8), 0 0 60px rgba(168, 85, 247, 0.5);
}
}
.animate-glow-pulse {
animation: glow-pulse 2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.animate-float { .animate-float {
animation: none; animation: none;
} }
.animate-glow-pulse {
animation: none;
}
} }
.card-hover:hover { .card-hover:hover {

View File

@ -64,31 +64,185 @@ export default function Report() {
return `${baseUrl}${content.pptPath}`; return `${baseUrl}${content.pptPath}`;
}; };
// Remove margins and padding from iframe content // Enhance report styling
const handleReportLoad = (e: React.SyntheticEvent<HTMLIFrameElement>) => { const handleReportLoad = (e: React.SyntheticEvent<HTMLIFrameElement>) => {
try { try {
const iframe = e.currentTarget; const iframe = e.currentTarget;
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
if (iframeDoc) { if (iframeDoc && iframeDoc.body) {
// Wrap all body content in a container if not already wrapped
if (!iframeDoc.querySelector('.sapiens-report-wrapper')) {
const wrapper = iframeDoc.createElement('div');
wrapper.className = 'sapiens-report-wrapper';
while (iframeDoc.body.firstChild) {
wrapper.appendChild(iframeDoc.body.firstChild);
}
iframeDoc.body.appendChild(wrapper);
}
const style = iframeDoc.createElement('style'); const style = iframeDoc.createElement('style');
style.textContent = ` style.textContent = `
html, body { html, body {
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%) !important;
max-width: none !important;
width: 100% !important;
} }
body > * {
margin: 0 !important; body {
padding: 48px 32px !important;
} }
h1, h2, h3, h4, h5, h6, p, ul, ol, figure, blockquote {
.sapiens-report-wrapper {
background: white !important;
border-radius: 16px !important;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1) !important;
padding: 56px !important;
max-width: 1000px !important;
margin: 0 auto !important;
}
h1 {
font-size: 36px !important;
font-weight: 700 !important;
color: #1a202c !important;
margin-bottom: 28px !important;
margin-top: 0 !important; margin-top: 0 !important;
padding-bottom: 20px !important;
border-bottom: 3px solid #3b82f6 !important;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
} }
.report, .container, .page, .content {
margin: 0 !important; h2 {
padding: 8px !important; font-size: 26px !important;
font-weight: 600 !important;
color: #2d3748 !important;
margin-top: 44px !important;
margin-bottom: 18px !important;
padding-bottom: 10px !important;
border-bottom: 2px solid #e2e8f0 !important;
} }
h3 {
font-size: 22px !important;
font-weight: 600 !important;
color: #374151 !important;
margin-top: 32px !important;
margin-bottom: 14px !important;
}
h4 {
font-size: 19px !important;
font-weight: 600 !important;
color: #4b5563 !important;
margin-top: 26px !important;
margin-bottom: 12px !important;
}
p {
font-size: 17px !important;
line-height: 1.8 !important;
color: #374151 !important;
margin-bottom: 18px !important;
text-align: justify !important;
}
.metadata {
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%) !important;
padding: 24px !important;
border-radius: 12px !important;
border-left: 4px solid #3b82f6 !important;
margin-bottom: 36px !important;
font-size: 15px !important;
color: #1e40af !important;
text-align: center !important;
}
.section {
margin-bottom: 36px !important;
}
ul, ol {
margin-bottom: 20px !important;
padding-left: 32px !important;
}
li {
font-size: 17px !important;
line-height: 1.8 !important;
color: #4b5563 !important;
margin-bottom: 10px !important;
}
.highlight {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%) !important;
padding: 24px !important;
border-left: 4px solid #f59e0b !important;
border-radius: 10px !important;
margin: 28px 0 !important;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.08) !important;
}
.quote {
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%) !important;
padding: 24px 28px !important;
border-left: 4px solid #6b7280 !important;
border-radius: 10px !important;
margin: 28px 0 !important;
font-style: italic !important;
color: #374151 !important;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.08) !important;
}
strong {
font-weight: 700 !important;
color: #1f2937 !important;
}
img {
max-width: 100% !important;
height: auto !important;
border-radius: 12px !important;
box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.15) !important;
margin: 20px 0 !important;
}
table {
max-width: 100% !important;
width: 100% !important;
border-collapse: collapse !important;
margin: 20px 0 !important;
}
table, iframe {
max-width: 100% !important;
}
@page { @page {
margin: 0 !important; margin: 0 !important;
} }
@media (max-width: 768px) {
.sapiens-report-wrapper {
padding: 32px 24px !important;
}
h1 {
font-size: 28px !important;
}
h2 {
font-size: 22px !important;
}
h3 {
font-size: 19px !important;
}
}
`; `;
iframeDoc.head.appendChild(style); iframeDoc.head.appendChild(style);
} }
@ -244,7 +398,7 @@ export default function Report() {
</header> </header>
{/* Main Content */} {/* Main Content */}
<main className="flex-1 max-w-7xl mx-auto px-2 py-2 pb-32 w-full"> <main className="flex-1 max-w-[1600px] mx-auto px-8 py-2 pb-32 w-full">
<Tabs defaultValue="report" className="w-full"> <Tabs defaultValue="report" className="w-full">
<TabsList className="grid w-full max-w-md mx-auto grid-cols-2 mb-4"> <TabsList className="grid w-full max-w-md mx-auto grid-cols-2 mb-4">
<TabsTrigger value="report" data-testid="tab-report"> <TabsTrigger value="report" data-testid="tab-report">

View File

@ -69,6 +69,16 @@ export async function registerRoutes(app: Express): Promise<Server> {
}); });
// Article routes // Article routes
app.get('/api/articles', async (req, res) => {
try {
const articles = await storage.getArticles();
res.json(articles);
} catch (error) {
console.error("Error fetching articles:", error);
res.status(500).json({ message: "Failed to fetch articles" });
}
});
app.get('/api/media-outlets/:slug/articles', async (req, res) => { app.get('/api/media-outlets/:slug/articles', async (req, res) => {
try { try {
const outlet = await storage.getMediaOutletBySlug(req.params.slug); const outlet = await storage.getMediaOutletBySlug(req.params.slug);

View File

@ -43,6 +43,7 @@ export interface IStorage {
updateMediaOutlet(id: string, outlet: Partial<InsertMediaOutlet>): Promise<MediaOutlet>; updateMediaOutlet(id: string, outlet: Partial<InsertMediaOutlet>): Promise<MediaOutlet>;
// Article operations // Article operations
getArticles(): Promise<Article[]>;
getArticlesByOutlet(mediaOutletId: string): Promise<Article[]>; getArticlesByOutlet(mediaOutletId: string): Promise<Article[]>;
getArticleBySlug(slug: string): Promise<Article | undefined>; getArticleBySlug(slug: string): Promise<Article | undefined>;
createArticle(article: InsertArticle): Promise<Article>; createArticle(article: InsertArticle): Promise<Article>;
@ -144,6 +145,13 @@ export class DatabaseStorage implements IStorage {
} }
// Article operations // Article operations
async getArticles(): Promise<Article[]> {
return await db
.select()
.from(articles)
.orderBy(desc(articles.publishedAt));
}
async getArticlesByOutlet(mediaOutletId: string): Promise<Article[]> { async getArticlesByOutlet(mediaOutletId: string): Promise<Article[]> {
return await db return await db
.select() .select()