Compare commits
10 Commits
c722f4e075
...
0eee17a43e
| Author | SHA1 | Date | |
|---|---|---|---|
| 0eee17a43e | |||
| b5e8cf3d3a | |||
| d4d521b40b | |||
| 4ff91ab090 | |||
| 7c5025ee10 | |||
| 52ccfc324c | |||
| 730720251a | |||
| bf5ff95c0e | |||
| 0894a95fa9 | |||
| 7395b0e038 |
4
.replit
4
.replit
@ -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"
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user