From 07497b46484e53b48bb7c47f3504e4eac39143e1 Mon Sep 17 00:00:00 2001 From: TerribleDev <1020010-TerribleDev@users.noreply.replit.com> Date: Tue, 18 Feb 2025 22:26:43 +0000 Subject: [PATCH] Restored to '5c89fd2cd9402ccde43ea7be18e5de6d1726a9fc' Replit-Restored-To:5c89fd2cd9402ccde43ea7be18e5de6d1726a9fc --- README.md | 70 -------------- client/public/manifest.json | 11 +-- client/src/pages/home.tsx | 71 +------------- server/routes.ts | 181 ++++++++++++++---------------------- server/utils.ts | 40 +++----- 5 files changed, 88 insertions(+), 285 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 44682c1..0000000 --- a/README.md +++ /dev/null @@ -1,70 +0,0 @@ - -# Newsletter Embed Route Documentation - -This documentation explains how to embed The Downtowner newsletter content into any website using our embed route. - -## Quick Start - -Add the following code to your website where you want the newsletter content to appear: - -```html - - - -``` - -## Features - -- Responsive grid layout -- Dark mode support -- Style isolation using Shadow DOM -- Displays up to 6 recent newsletters -- Each newsletter card includes: - - Title - - Publication date - - Thumbnail image (if available) - - Description - - Read more link - -## Customization - -The embed route uses CSS variables for theming. You can override these variables in your website's CSS: - -```css -newsletter-embed { - --background: #ffffff; - --foreground: #000000; - --card: #ffffff; - --card-foreground: #000000; - --primary: #000000; - --border: #e2e8f0; -} -``` - -## Demo - -You can view a live demo of the embed functionality at `/embed-demo.html` on your Repl. - -## CORS - -The embed route has CORS enabled, allowing it to be embedded on any domain. diff --git a/client/public/manifest.json b/client/public/manifest.json index cd8edfe..2b2869b 100644 --- a/client/public/manifest.json +++ b/client/public/manifest.json @@ -10,14 +10,7 @@ { "src": "/icon.png", "sizes": "192x192", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "/icon-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any maskable" + "type": "image/png" } ] -} \ No newline at end of file +} diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index 844722f..b3ac550 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -11,10 +11,11 @@ import { Calendar, RefreshCw, Share2, + Twitter, + Facebook, + Rss, Bell, - BellOff, - Download, - Rss + BellOff } from "lucide-react"; import { useNewsletters, useNewsletterSearch } from "@/lib/newsletter-data"; import { useToast } from "@/hooks/use-toast"; @@ -29,8 +30,6 @@ export default function Home() { const [isImporting, setIsImporting] = useState(false); const [page, setPage] = useState(1); const [isSubscribed, setIsSubscribed] = useState(false); - const [deferredPrompt, setDeferredPrompt] = useState(null); - const [isInstallable, setIsInstallable] = useState(false); const loader = useRef(null); const { data: allNewsletters, isLoading, isFetching } = useNewsletters(); const { data: searchResults } = useNewsletterSearch(searchQuery); @@ -40,56 +39,6 @@ export default function Home() { const paginatedNewsletters = newsletters?.slice(0, page * ITEMS_PER_PAGE); const isDevelopment = import.meta.env.MODE === 'development'; - useEffect(() => { - // Listen for the beforeinstallprompt event - window.addEventListener('beforeinstallprompt', (e) => { - // Prevent Chrome 67 and earlier from automatically showing the prompt - e.preventDefault(); - // Stash the event so it can be triggered later - setDeferredPrompt(e); - setIsInstallable(true); - }); - - // Listen for successful installation - window.addEventListener('appinstalled', () => { - setIsInstallable(false); - setDeferredPrompt(null); - toast({ - title: "Success", - description: "The Downtowner has been installed!", - }); - }); - }, [toast]); - - const handleInstall = async () => { - if (!deferredPrompt) return; - - try { - // Show the install prompt - deferredPrompt.prompt(); - // Wait for the user to respond to the prompt - const { outcome } = await deferredPrompt.userChoice; - - if (outcome === 'accepted') { - toast({ - title: "Installing...", - description: "The Downtowner is being installed on your device", - }); - } - - // Clear the deferredPrompt for the next time - setDeferredPrompt(null); - setIsInstallable(false); - } catch (error) { - console.error('Error installing PWA:', error); - toast({ - title: "Error", - description: "Failed to install the application", - variant: "destructive", - }); - } - }; - const handleImport = async () => { try { setIsImporting(true); @@ -248,22 +197,12 @@ export default function Home() { )} - {isInstallable && ( - - )} diff --git a/server/routes.ts b/server/routes.ts index e17e9ab..a7fdb01 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -13,29 +13,25 @@ const vapidPublicKey = process.env.VAPID_PUBLIC_KEY; const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY; if (!vapidPublicKey || !vapidPrivateKey) { - throw new Error( - "VAPID keys are required for push notifications. Please set VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY environment variables.", - ); + throw new Error('VAPID keys are required for push notifications. Please set VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY environment variables.'); } webpush.setVapidDetails( - "mailto:team@downtowner.com", + 'mailto:team@downtowner.com', vapidPublicKey, - vapidPrivateKey, + vapidPrivateKey ); export async function registerRoutes(app: Express): Promise { // Setup background job to check for new newsletters - schedule.scheduleJob("0 */4 * * *", async function () { + schedule.scheduleJob('0 */6 * * *', async function() { try { const existingNewsletters = await storage.getNewsletters(); let newNewslettersCount = 0; await scrapeNewsletters(async (newsletter) => { // Check if newsletter already exists - const exists = existingNewsletters.some( - (existing) => existing.url === newsletter.url, - ); + const exists = existingNewsletters.some(existing => existing.url === newsletter.url); if (!exists) { await storage.importNewsletter(newsletter); newNewslettersCount++; @@ -44,52 +40,38 @@ export async function registerRoutes(app: Express): Promise { }); if (newNewslettersCount > 0) { - console.log( - `Found ${newNewslettersCount} new newsletters, sending notifications...`, - ); + console.log(`Found ${newNewslettersCount} new newsletters, sending notifications...`); // Send push notifications for new newsletters const subscriptions = await storage.getActiveSubscriptions(); - console.log( - `Sending notifications to ${subscriptions.length} subscribers`, - ); + console.log(`Sending notifications to ${subscriptions.length} subscribers`); const notificationPayload = JSON.stringify({ - title: "New Newsletters Available", - body: `${newNewslettersCount} new newsletter${newNewslettersCount > 1 ? "s" : ""} published!`, - icon: "/icon.png", + title: 'New Newsletters Available', + body: `${newNewslettersCount} new newsletter${newNewslettersCount > 1 ? 's' : ''} published!`, + icon: '/icon.png' }); const results = await Promise.allSettled( - subscriptions.map((subscription) => - webpush.sendNotification( - { - endpoint: subscription.endpoint, - keys: { - auth: subscription.auth, - p256dh: subscription.p256dh, - }, - }, - notificationPayload, - ), - ), + subscriptions.map(subscription => + webpush.sendNotification({ + endpoint: subscription.endpoint, + keys: { + auth: subscription.auth, + p256dh: subscription.p256dh + } + }, notificationPayload) + ) ); - const succeeded = results.filter( - (r) => r.status === "fulfilled", - ).length; - const failed = results.filter((r) => r.status === "rejected").length; - console.log( - `Push notifications sent: ${succeeded} succeeded, ${failed} failed`, - ); + const succeeded = results.filter(r => r.status === 'fulfilled').length; + const failed = results.filter(r => r.status === 'rejected').length; + console.log(`Push notifications sent: ${succeeded} succeeded, ${failed} failed`); } // Retry fetching details for newsletters without them - const newslettersWithoutDetails = - await storage.getNewslettersWithoutDetails(); - const updatedNewsletters = await retryMissingDetails( - newslettersWithoutDetails, - ); + const newslettersWithoutDetails = await storage.getNewslettersWithoutDetails(); + const updatedNewsletters = await retryMissingDetails(newslettersWithoutDetails); for (const newsletter of updatedNewsletters) { if (newsletter.id) { @@ -102,8 +84,9 @@ export async function registerRoutes(app: Express): Promise { console.log(`Updated details for newsletter: ${newsletter.title}`); } } + } catch (error) { - console.error("Background job failed:", error); + console.error('Background job failed:', error); } }); @@ -237,43 +220,30 @@ export async function registerRoutes(app: Express): Promise { `; - res.header("Content-Type", "text/html"); + res.header('Content-Type', 'text/html'); res.send(content); } catch (error) { - console.error("Error generating embedded content:", error); + console.error('Error generating embedded content:', error); res.status(500).json({ message: "Failed to generate embedded content" }); } }); @@ -285,7 +255,7 @@ export async function registerRoutes(app: Express): Promise { }); app.get("/api/newsletters/search", async (req, res) => { - const query = (req.query.q as string) || ""; + const query = req.query.q as string || ""; const newsletters = await storage.searchNewsletters(query); res.json(newsletters); }); @@ -297,64 +267,55 @@ export async function registerRoutes(app: Express): Promise { await storage.importNewsletter(newsletter); importedCount++; }); - res.json({ - message: `Successfully imported ${importedCount} newsletters`, - }); + res.json({ message: `Successfully imported ${importedCount} newsletters` }); } catch (error) { - console.error("Error importing newsletters:", error); + console.error('Error importing newsletters:', error); res.status(500).json({ message: "Failed to import newsletters" }); } }); app.post("/api/subscriptions", async (req, res) => { try { - console.log("Received subscription request:", { + console.log('Received subscription request:', { endpoint: req.body.endpoint, - auth: req.body.keys?.auth ? "[present]" : "[missing]", - p256dh: req.body.keys?.p256dh ? "[present]" : "[missing]", + auth: req.body.keys?.auth ? '[present]' : '[missing]', + p256dh: req.body.keys?.p256dh ? '[present]' : '[missing]' }); - if ( - !req.body.endpoint || - !req.body.keys?.auth || - !req.body.keys?.p256dh - ) { - throw new Error("Invalid subscription data"); + if (!req.body.endpoint || !req.body.keys?.auth || !req.body.keys?.p256dh) { + throw new Error('Invalid subscription data'); } await storage.addSubscription({ endpoint: req.body.endpoint, auth: req.body.keys.auth, - p256dh: req.body.keys.p256dh, + p256dh: req.body.keys.p256dh }); // Test the subscription with a welcome notification try { - await webpush.sendNotification( - { - endpoint: req.body.endpoint, - keys: { - auth: req.body.keys.auth, - p256dh: req.body.keys.p256dh, - }, - }, - JSON.stringify({ - title: "Subscription Successful", - body: "You will now receive notifications for new newsletters!", - icon: "/icon.png", - }), - ); - console.log("Welcome notification sent successfully"); + await webpush.sendNotification({ + endpoint: req.body.endpoint, + keys: { + auth: req.body.keys.auth, + p256dh: req.body.keys.p256dh + } + }, JSON.stringify({ + title: 'Subscription Successful', + body: 'You will now receive notifications for new newsletters!', + icon: '/icon.png' + })); + console.log('Welcome notification sent successfully'); } catch (notifError) { - console.error("Failed to send welcome notification:", notifError); + console.error('Failed to send welcome notification:', notifError); } res.json({ message: "Subscription added successfully" }); } catch (error) { - console.error("Error adding subscription:", error); + console.error('Error adding subscription:', error); res.status(500).json({ message: "Failed to add subscription", - error: error instanceof Error ? error.message : "Unknown error", + error: error instanceof Error ? error.message : 'Unknown error' }); } }); @@ -363,14 +324,12 @@ export async function registerRoutes(app: Express): Promise { try { const subscriptionId = parseInt(req.params.id); await storage.saveNotificationSettings(subscriptionId, { - newsletter_notifications: req.body.newsletter_notifications, + newsletter_notifications: req.body.newsletter_notifications }); res.json({ message: "Notification settings updated successfully" }); } catch (error) { - console.error("Error updating notification settings:", error); - res - .status(500) - .json({ message: "Failed to update notification settings" }); + console.error('Error updating notification settings:', error); + res.status(500).json({ message: "Failed to update notification settings" }); } }); @@ -386,13 +345,11 @@ export async function registerRoutes(app: Express): Promise { language: "en", copyright: "All rights reserved", favicon: "https://downtowner.com/favicon.ico", - updated: newsletters[0]?.date - ? new Date(newsletters[0].date) - : new Date(), + updated: newsletters[0]?.date ? new Date(newsletters[0].date) : new Date(), generator: "The Downtowner RSS Feed", feedLinks: { - rss2: "https://downtowner.com/api/rss", - }, + rss2: "https://downtowner.com/api/rss" + } }); for (const newsletter of newsletters) { @@ -400,20 +357,20 @@ export async function registerRoutes(app: Express): Promise { title: newsletter.title, id: newsletter.url, link: newsletter.url, - description: newsletter.description || "", + description: newsletter.description || '', date: new Date(newsletter.date), - image: newsletter.thumbnail || undefined, + image: newsletter.thumbnail || undefined }); } - res.type("application/xml"); + res.type('application/xml'); res.send(feed.rss2()); } catch (error) { - console.error("Error generating RSS feed:", error); + console.error('Error generating RSS feed:', error); res.status(500).json({ message: "Failed to generate RSS feed" }); } }); const httpServer = createServer(app); return httpServer; -} +} \ No newline at end of file diff --git a/server/utils.ts b/server/utils.ts index ab53754..bff8051 100644 --- a/server/utils.ts +++ b/server/utils.ts @@ -8,13 +8,9 @@ const ROBLY_ARCHIVE_URL = async function scrapeNewsletterContent( url: string, retryCount = 0, -): Promise<{ - thumbnail: string | null; - content: string | null; - hasDetails: boolean; -}> { +): Promise<{ thumbnail: string | null; content: string | null; hasDetails: boolean }> { try { - const backoffTime = 60000; // 1 minute + const backoffTime = Math.min(1000 * Math.pow(2, retryCount), 1000); if (retryCount > 0) { await new Promise((resolve) => setTimeout(resolve, backoffTime)); } @@ -54,7 +50,7 @@ async function scrapeNewsletterContent( } catch (error: any) { if ( (error.response?.status === 429 || error.code === "ECONNRESET") && - retryCount < 5 + retryCount < 1 ) { console.log( `Rate limited or connection reset, attempt ${retryCount + 1}/5`, @@ -67,7 +63,7 @@ async function scrapeNewsletterContent( } export async function scrapeNewsletters( - onNewsletterProcessed?: (newsletter: InsertNewsletter) => Promise, + onNewsletterProcessed?: (newsletter: InsertNewsletter) => Promise ): Promise { try { const { data } = await axios.get(ROBLY_ARCHIVE_URL, { @@ -100,8 +96,7 @@ export async function scrapeNewsletters( const date = new Date(dateStr).toISOString().split("T")[0]; const fullUrl = `https://app.robly.com${url}`; - const { thumbnail, content, hasDetails } = - await scrapeNewsletterContent(fullUrl); + const { thumbnail, content, hasDetails } = await scrapeNewsletterContent(fullUrl); const newsletter: InsertNewsletter = { title: title.trim(), @@ -118,9 +113,7 @@ export async function scrapeNewsletters( } newsletters.push(newsletter); - console.log( - `Processed newsletter: ${title} (hasDetails: ${hasDetails})`, - ); + console.log(`Processed newsletter: ${title} (hasDetails: ${hasDetails})`); } catch (err) { console.warn( "Error processing date for newsletter:", @@ -154,21 +147,15 @@ export async function scrapeNewsletters( } } -export async function retryMissingDetails( - newsletters: Newsletter[], -): Promise { - const newslettersWithoutDetails = newsletters.filter((n) => !n.hasDetails); - console.log( - `Found ${newslettersWithoutDetails.length} newsletters without details to retry`, - ); +export async function retryMissingDetails(newsletters: Newsletter[]): Promise { + const newslettersWithoutDetails = newsletters.filter(n => !n.hasDetails); + console.log(`Found ${newslettersWithoutDetails.length} newsletters without details to retry`); const updatedNewsletters: InsertNewsletter[] = []; for (const newsletter of newslettersWithoutDetails) { try { - const { thumbnail, content, hasDetails } = await scrapeNewsletterContent( - newsletter.url, - ); + const { thumbnail, content, hasDetails } = await scrapeNewsletterContent(newsletter.url); if (hasDetails) { updatedNewsletters.push({ @@ -181,12 +168,9 @@ export async function retryMissingDetails( console.log(`Successfully retrieved details for: ${newsletter.title}`); } } catch (error) { - console.error( - `Failed to retrieve details for ${newsletter.title}:`, - error, - ); + console.error(`Failed to retrieve details for ${newsletter.title}:`, error); } } return updatedNewsletters; -} +} \ No newline at end of file