Skip to content

Vikx001/newsly

Repository files navigation

Newsly β€” Ultra-Short News App

A React-based cross-platform news app that delivers ultra-short (β‰ˆ60-word) news stories with swipeable cards. Built with React + Capacitor for web and native mobile deployment.

πŸ“Έ Screenshots β€” Google Pixel 9

Landing Page Genre Selection
Landing Page Genre Selection
News Feed Settings
News Feed Settings

πŸš€ Features

πŸ“± Core Experience

  • Cross-Platform: Runs on Web, Android (APK/AAB), and iOS (planned) via Capacitor 5
  • Ultra-Short Summaries: News stories condensed to ~60 words β€” no fluff, just facts
  • TikTok-style Swipe: Two-card stack with translateY transitions, double-rAF two-phase animation, 380ms cubic-bezier easing β€” swipe or use arrow keys
  • Real-time News: Live Google News RSS feeds, auto-refreshes on country or genre change without a full page reload
  • Infinite Scroll: Automatically appends more articles (deduped by URL) when within 3 cards of the end
  • Reading Time: Per-article word-count estimate displayed on each card (~200 wpm)

🎨 UI & Design

  • Dark / Light Mode: Toggle on every screen β€” Landing, Genre Selection, Feed, and Settings; defaults to light, persisted to localStorage
  • Redesigned Landing Page: Hero layout with 2-column feature grid (Zap / Globe2 / Bookmark / ShieldCheck), theme toggle, and Skip button
  • Redesigned Genre Selection: Dark 2-column tile grid with per-genre gradient colors, animated checkmark badge on selected tiles, scale bump, sticky progress bar + CTA
  • Redesigned Settings Page: Profile strip with gradient icon, grouped Section cards (rounded-2xl), SettingRow with colored icon pills + Toggle switch, bottom-sheet backdrop-blur modals
  • Swipe-proof Header: Touch events outside the card are captured to prevent the header from being dragged away β€” inner text panel remains independently scrollable

πŸ” Search

  • Debounced Search: 400ms debounce so results only filter after you stop typing
  • Article / Newspaper Toggle: Switch between searching article titles+descriptions vs. source names (e.g. "BBC", "Reuters", "The Hindu")
  • Inline No-Results Fallback: When a search yields zero results, a friendly card replaces the stack with hint text and quick-action buttons (Clear search / Switch mode) β€” you never leave the feed
  • Typing Indicator: "Searching…" overlay shown during the debounce window

🌍 Personalisation

  • Country Selection: 15+ countries with flag indicators β€” news scoped to your region, passed server-side via countryParam
  • Genre Selection: 8 categories β€” Technology, World, Business, Sports, Science, Health, Entertainment, Politics
  • Default Sort: Toggle between Personalized feed or Latest news
  • Hide Paywalled Articles: Filter out articles from 30 known paywalled sources (expanded from 9)
  • Font Size: Small / Medium / Large reading preference

πŸ–ΌοΈ Smart Images

  • Wikipedia Image Pipeline: MediaWiki search+pageimages API with keyword extraction, financial stop-word filtering, per-session module-level cache, and CapacitorHttp on Android native to bypass WebView CORS

πŸ”– Saves & Social

  • Bookmarking: Save articles to a dedicated Bookmarks page, persisted to localStorage
  • Comments System: Inline name prompt, 500-character limit with live counter, like deduplication keyed by hashKey β€” no window.prompt(), no repeat likes
  • Share Functionality: Native share sheet on mobile, clipboard fallback on web
  • Read Aloud: SpeechManager singleton (Web Speech API TTS) β€” listen to articles hands-free

🧠 AI & Analysis

  • Bias Analysis: Community bias panel + vote sheet keyed by hashKey
  • Enhanced Bias Mode: When enabled in Settings, auto-runs /api/ai/bias-analysis on every article load and pre-fills the score

πŸ›‘οΈ Reliability & Security

  • Error Boundary: React class component wraps the entire app β€” catches render crashes, shows "Try again" UI
  • hashKey (djb2): All localStorage keys derived from article IDs use a collision-resistant djb2 hash instead of btoa() β€” safe against special characters and encoding collisions
  • Serverless-First Fetching: api.js tries /api/news (Vercel serverless, true server-side CORS bypass) first; falls back to allorigins.win CORS proxy for local dev / fallback
  • Progress Indicator: X / Y counter below the card stack

βš™οΈ Settings

  • Appearance: Dark/light theme toggle
  • Feed: Hide paywalled (30 domains), default sort mode, Enhanced Bias Analysis
  • Notifications: On/off toggle
  • Account: Bookmarks, Subscriptions (stub), Clear History, Feedback, Help & Support
  • Danger Zone: Log out (clears localStorage)

πŸ“… Development Timeline

Date Version Updates
19/03/2026 v3.1.0 πŸ” Search, Reliability & UX Polish
β€’ Debounced search (400 ms) with Article / Newspaper toggle - filter by title+description or by source name.
β€’ Inline no-results fallback - friendly card with Clear search and Switch mode CTAs replaces card stack; never kicks user out of the feed.
β€’ Typing indicator overlay during debounce window.
β€’ ErrorBoundary component wraps entire app - catches render crashes with Try again UI.
β€’ hashKey (djb2) replaces btoa() for all localStorage keys - collision-resistant, safe against special characters.
β€’ CommentsCard rewrite - inline name prompt (no window.prompt()), 500-char limit with live counter, like deduplication per article.
β€’ SpeechManager singleton (speech.js) replaces ad-hoc inline speech code in NewsCard.
β€’ Reading time estimate per article (word-count / 200 wpm).
β€’ Enhanced Bias auto-run - when enabled in Settings, bias score fetched automatically on article load.
β€’ Serverless-first fetch - api.js now tries /api/news (Vercel, true server-side bypass) before CORS proxy fallback.
β€’ Country-aware serverless - api/news.js accepts countryParam query param; builds country-scoped RSS URLs server-side.
β€’ Infinite scroll - auto-appends more articles (URL-deduped) when within 3 cards of end.
β€’ Progress indicator - X / Y counter below card stack.
β€’ No reload on country change - loadNews(forceRefresh, countryOverride) replaces window.location.reload().
β€’ Swipe-proof header - touchAction: none plus gesture capture on main prevents header from being swiped away; inner text panel independently scrollable.
β€’ Paywall list expanded 9 to 30 domains (Telegraph, Wired, HBR, Nature, Barrons, regional papers, AFR, etc.).
β€’ Dead code removed: HeaderBar.jsx, SettingsModal.jsx, UnderConstruction.jsx.
19/03/2026 v3.1.0 πŸ” Search, Reliability & UX Polish
β€’ Debounced search (400 ms) with Article / Newspaper toggle - filter by title+description or by source name.
β€’ Inline no-results fallback - friendly card with Clear search and Switch mode CTAs replaces card stack; never kicks user out of the feed.
β€’ Typing indicator overlay during debounce window.
β€’ ErrorBoundary component wraps entire app - catches render crashes with Try again UI.
β€’ hashKey (djb2) replaces btoa() for all localStorage keys - collision-resistant, safe against special characters.
β€’ CommentsCard rewrite - inline name prompt (no window.prompt()), 500-char limit with live counter, like deduplication per article.
β€’ SpeechManager singleton (speech.js) replaces ad-hoc inline speech code in NewsCard.
β€’ Reading time estimate per article (word-count / 200 wpm).
β€’ Enhanced Bias auto-run - when enabled in Settings, bias score fetched automatically on article load.
β€’ Serverless-first fetch - api.js now tries /api/news (Vercel, true server-side bypass) before CORS proxy fallback.
β€’ Country-aware serverless - api/news.js accepts countryParam query param; builds country-scoped RSS URLs server-side.
β€’ Infinite scroll - auto-appends more articles (URL-deduped) when within 3 cards of end.
β€’ Progress indicator - X / Y counter below card stack.
β€’ No reload on country change - loadNews(forceRefresh, countryOverride) replaces window.location.reload().
β€’ Swipe-proof header - touchAction: none plus gesture capture on main prevents header from being swiped away; inner text panel independently scrollable.
β€’ Paywall list expanded 9 to 30 domains (Telegraph, Wired, HBR, Nature, Barrons, regional papers, AFR, etc.).
β€’ Dead code removed: HeaderBar.jsx, SettingsModal.jsx, UnderConstruction.jsx.
15/03/2026 v3.0.0 🎨 Full UI Overhaul & Bug Fixes
β€’ TikTok-style swipe animation: two-card stack with translateY transitions, double-rAF two-phase animation, 380 ms cubic-bezier easing.
β€’ Wikipedia Smart Images: switched from page/summary to MediaWiki search+pageimages API; keyword extraction strips financial stop words; per-session module-level cache; CapacitorHttp on Android native to bypass WebView CORS.
β€’ Flicker & white-flash fixes: removed synchronous setResolvedImage(null) and isTransitioning opacity animation from Feed.
β€’ Country dropdown z-index fix: Feed header gets relative z-50 so selector renders above card stack.
β€’ Landing page redesign: dark/light hero layout, 2-column feature grid (Zap / Globe2 / Bookmark / ShieldCheck icons), sun/moon toggle, Skip button.
β€’ Genre Selection redesign: dark 2-column square tile grid with per-genre gradient colors, checkmark badge on selected tiles, scale bump, sticky progress bar + CTA.
β€’ Settings page redesign: Profile strip with gradient icon, grouped Section cards (rounded-2xl), SettingRow with colored icon pills + Toggle switch, bottom-sheet backdrop-blur modals; sections: Appearance / Feed / Notifications / Account / Support / Danger zone.
β€’ Theme toggle added to Landing and Genre Selection (uses existing ThemeContext, defaults to light).
β€’ ~15 miscellaneous bug fixes (Bengali locale, Android build, image cache race conditions, etc.)
16/08/2025 v2.3.0 🎨 Layout Refresh & Settings Expansion
β€’ New compact header in Feed with country selector, refresh, sort (Personalized/Latest), theme and settings buttons.
β€’ β€œSwipe up” affordance and smoother card transitions.
β€’ Read Aloud controls and keyboard shortcuts (Arrow Up/Down, Ctrl+Space).
β€’ Community Bias features: analysis panel + vote sheet with local persistence.
β€’ Article translation with LibreTranslate/Lingva fallback.
β€’ Settings additions: Theme, Notifications, Reading font size, Hide paywalled, Default sort, Bookmarks, Subscriptions (stub), Clear history, Feedback, Help & Support, Logout
16/08/2025 v2.2.0 πŸ–ΌοΈ Smart Image Resolver & Reliability
β€’ Prefer original article URL (bypass Google News redirect).
β€’ Extract images from OG/Twitter/JSON‑LD/srcset and follow canonical links.
β€’ Openverse photograph fallback when no image is found.
β€’ Web image proxying for reliability (Weserv).
β€’ Improved handling of placeholder/flag images
30/01/2025 v2.1.0 🌍 Country Selection Feature
β€’ Added 15+ country support with flag indicators.
β€’ Auto-refresh on country change.
β€’ Visual loading states for country selector.
β€’ Improved refresh button feedback
29/01/2025 v2.0.0 πŸ”§ Major UI/UX Improvements
β€’ Fixed mobile external URL navigation.
β€’ TikTok-style swipe navigation.
β€’ Dark/Light theme toggle.
β€’ Comments system with local storage.
β€’ Responsive design for mobile/desktop.
β€’ Share functionality.
β€’ Capacitor integration for mobile apps.
β€’ Mobile "Read More" button fixes
28/01/2025 v1.5.0 πŸ“± Mobile Optimization
β€’ Enhanced swipe gestures.
β€’ Improved touch responsiveness.
β€’ Better mobile UI components
27/01/2025 v1.0.0 πŸŽ‰ Initial Release
β€’ Core news fetching functionality.
β€’ Genre selection.
β€’ Basic UI components.
β€’ Web deployment ready

πŸ—οΈ Architecture Overview

App Layer

flowchart TD
    A(["πŸ‘€ User"])

    A --> Landing

    subgraph Pages["πŸ—‚οΈ Pages β€” React Router v6"]
        direction TB
        Landing["🏠 Landing\nHero · Feature Grid · Theme Toggle"]
        Genre["🎯 GenreSelection\n2-col Gradient Tile Grid · Progress Bar"]
        Feed["πŸ“° Feed\nTikTok Swipe Stack Β· Country Selector"]
        Bookmarks["πŸ”– Bookmarks\nSaved Articles List"]
        Settings["βš™οΈ Settings\nSections Β· Toggles Β· Bottom-sheet Modals"]

        Landing -->|"Get Started"| Genre
        Genre -->|"Confirm genres"| Feed
        Feed --> Bookmarks
        Feed --> Settings
    end

    subgraph Components["🧩 Shared Components"]
        direction TB
        NewsCard["πŸ“„ NewsCard\nSwipe Β· Reading Time Β· Bias Auto-run"]
        ErrorBoundary["πŸ›‘οΈ ErrorBoundary\nRender Error Catch Β· Retry UI"]
        CountrySelector["🌍 CountrySelector\nFlag Dropdown β€” z-50 above stack"]
        CommentsCard["πŸ’¬ CommentsCard\nInline Name Β· 500-char Limit Β· Dedup"]
    end

    subgraph State["πŸ—ƒοΈ Global State β€” React Context"]
        direction LR
        ThemeCtx["πŸŒ™ ThemeContext\nisDark / toggleTheme\nβ†’ localStorage"]
        BookmarkCtx["πŸ”– BookmarkContext\nbookmarks[]\nβ†’ localStorage"]
    end

    subgraph Utils["πŸ› οΈ Utils & Services"]
        direction LR
        apiJs["api.js\nserverless-first fetchNews()"]
        mockApi["mockApi.js\nXML β†’ article[] Β· getCountryParam()"]
        storageJs["storage.js\nget/set helpers Β· hashKey (djb2)"]
        speechJs["speech.js\nSpeechManager singleton"]
        genresJs["genres.js\nGradient map"]
    end

    Feed --> NewsCard
    Feed --> CountrySelector
    NewsCard --> CommentsCard
    App --> ErrorBoundary
    Feed --> apiJs
    apiJs --> mockApi
    Settings --> ThemeCtx
    Settings --> storageJs
    Feed --> BookmarkCtx

    classDef page fill:#dbeafe,stroke:#2563eb,color:#0f172a,rx:8
    classDef component fill:#dcfce7,stroke:#16a34a,color:#0f172a,rx:8
    classDef ctx fill:#fef9c3,stroke:#ca8a04,color:#0f172a,rx:8
    classDef util fill:#ede9fe,stroke:#7c3aed,color:#0f172a,rx:8
    classDef user fill:#f1f5f9,stroke:#475569,color:#0f172a,rx:20

    class Landing,Genre,Feed,Bookmarks,Settings page
    class NewsCard,ErrorBoundary,CountrySelector,CommentsCard component
    class ThemeCtx,BookmarkCtx ctx
    class apiJs,mockApi,storageJs,speechJs,genresJs util
    class A user
Loading

Data & Platform Layer

flowchart TD
    subgraph Fetch["πŸ“‘ News Fetching β€” api.js"]
        direction TB
        Check{"Running on\nnative platform?"}
        Serverless["POST /api/news\nVercel serverless (tried first)"]
        CapHttp["CapacitorHttp.get\nBypasses WebView CORS"]
        Proxy["fetch β†’ allorigins.win\nCORS proxy fallback"]
        Check -->|"Android / iOS"| CapHttp
        Check -->|"Web β€” try serverless"| Serverless
        Serverless -->|"fails"| Proxy
    end

    subgraph RSS["☁️ Google News RSS"]
        direction TB
        GRSS["news.google.com/rss\nper country hl/gl param\n+ genre topic URL"]
    end

    subgraph Images["πŸ–ΌοΈ Wikipedia Image Pipeline β€” NewsCard"]
        direction TB
        Query["buildWikiQuery\nstrip tickers + stop-words\ntop-5 keywords"]
        Wiki["MediaWiki API\nsearch + pageimages"]
        Cache["resolvedImageCache\nmodule-level Map\n(per session)"]
        Query --> Wiki --> Cache
    end

    subgraph Serverless["☁️ Vercel Serverless"]
        direction TB
        NewsAPI["api/news.js\nServer-side RSS fetch"]
        BiasAPI["api/ai/bias-analysis.js\nAI bias scoring"]
    end

    subgraph Platform["πŸ“² Platform Layer β€” Capacitor 5"]
        direction LR
        Web["🌐 Web\nVite dist · Vercel CDN"]
        Android["πŸ€– Android\nAPK / AAB Β· CapacitorHttp"]
        iOS["🍎 iOS\nplanned"]
    end

    CapHttp --> GRSS
    Proxy --> GRSS
    NewsAPI -.->|server-side bypass| GRSS

    Fetch --> RSS
    Images -.->|image lookup| Wiki

    Platform --> Fetch
    Platform --> Images

    classDef fetch fill:#dbeafe,stroke:#2563eb,color:#0f172a,rx:8
    classDef rss fill:#fff7ed,stroke:#ea580c,color:#0f172a,rx:8
    classDef img fill:#dcfce7,stroke:#16a34a,color:#0f172a,rx:8
    classDef server fill:#fef9c3,stroke:#ca8a04,color:#0f172a,rx:8
    classDef platform fill:#f1f5f9,stroke:#475569,color:#0f172a,rx:8

    class Check,CapHttp,Proxy,Serverless fetch
    class GRSS rss
    class Query,Wiki,Cache img
    class NewsAPI,BiasAPI server
    class Web,Android,iOS platform
Loading

πŸ”„ News Feed Flow

flowchart TD
    A([User opens app]) --> B[Landing.jsx\nhero + feature grid]
    B -->|Get Started| C[GenreSelection.jsx\nselect 1–8 categories]
    C -->|Confirm β†’ genres saved\nto localStorage| D[Feed.jsx\nTikTok swipe stack]

    D --> E{fetchNews\ngenre + country}
    E --> F{Running on\nnative platform?}
    F -->|Yes β€” Android| G[CapacitorHttp.get\nbypasses WebView CORS]
    F -->|No β€” Web| H[fetch via\nallorigins.win proxy]
    G --> I[Google News RSS]
    H --> I
    I --> J[mockApi.js\nparseXML β†’ article array]
    J --> K[Render NewsCard stack\ntwo-card z-indexed]

    K --> L{User action}
    L -->|Swipe up / Arrow↑| M[Next article\ndouble-rAF translateY\n380ms cubic-bezier]
    L -->|Swipe down / Arrow↓| N[Prev article\nsame animation]
    L -->|Tap Bookmark| O[BookmarkContext\n→ localStorage]
    L -->|Tap Share| P[navigator.share\nor clipboard]
    L -->|Long-press / Comments| Q[CommentsCard\nlocal votes]
    L -->|Tap Read More| R[Open article URL\nin browser / app]

    K --> S[Wikipedia Image Pipeline]
    S --> T[buildWikiQuery\nstrip tickers + stop-words\ntop-5 keywords]
    T --> U{Platform?}
    U -->|Native| V[CapacitorHttp β†’ MediaWiki\nsearch+pageimages API]
    U -->|Web| W[fetch β†’ MediaWiki\nsearch+pageimages API]
    V --> X[resolvedImageCache\nmodule-level Map]
    W --> X
    X --> Y[img src in NewsCard]

    D --> Z[CountrySelector\nflag dropdown z-50]
    Z -->|setSelectedCountry| E

    classDef page fill:#e0f2fe,stroke:#2563eb,color:#0f172a
    classDef decision fill:#fef9c3,stroke:#ca8a04,color:#0f172a
    classDef external fill:#fff7ed,stroke:#ea580c,color:#0f172a
    classDef action fill:#f0fdf4,stroke:#16a34a,color:#0f172a
    classDef storage fill:#f5f3ff,stroke:#7c3aed,color:#0f172a

    class A,B,C,D,K page
    class E,F,L,U decision
    class I,V,W,R external
    class M,N,O,P,Q,S,T,X,Y action
    class O,X storage
Loading

πŸ”€ Swipe Animation Flow

flowchart TD
    A([Touch / Key event]) --> B{Direction?}
    B -->|Up / ArrowUp| C[pendingIndexRef = currentIndex + 1]
    B -->|Down / ArrowDown| D[pendingIndexRef = currentIndex - 1]

    C --> E[setAnimating true\nsetAnimDir up]
    D --> F[setAnimating true\nsetAnimDir down]

    E --> G[rAF 1 β€” layout paint]
    F --> G
    G --> H[rAF 2 β€” setAnimReady true\ntrigger CSS transition]
    H --> I[translateY current card:\nup β†’ -110vh  down β†’ +110vh\n380ms cubic-bezier 0.4,0,0.2,1]
    I --> J[onTransitionEnd]
    J --> K[setCurrentIndex = pendingIndexRef\nsetAnimating false\nsetAnimReady false]
    K --> L[New card snaps into place]

    classDef anim fill:#e0f2fe,stroke:#2563eb,color:#0f172a
    classDef trigger fill:#f0fdf4,stroke:#16a34a,color:#0f172a
    classDef state fill:#fef9c3,stroke:#ca8a04,color:#0f172a

    class A,B trigger
    class C,D,E,F,G,H,I anim
    class J,K,L state
Loading

πŸ—‚οΈ localStorage Data Model

erDiagram
    APP_STATE {
        string newsly-theme "light | dark"
        string newsly-genres "JSON array of selected genre ids"
        string newsly-country "country code e.g. us, in, gb"
        string newsly-sort "personalized | latest"
    }

    ARTICLE_INTERACTIONS {
        string newsly-bookmarks "JSON array of article objects"
        string newsly-comments "JSON map hashKey(articleId) β†’ comment[]"
        string newsly-bias-votes "JSON map hashKey(articleId) β†’ vote"
        string newsly-liked-comments "JSON map hashKey(articleId) β†’ Set of liked comment ids"
        string newsly-reading-history "JSON array of viewed article ids"
    }

    PREFERENCES {
        string newsly-notifications "true | false"
        string newsly-font-size "small | medium | large"
        string newsly-hide-paywalled "true | false"
        string newsly-enhanced-bias "true | false"
    }

    APP_STATE ||--o{ ARTICLE_INTERACTIONS : "drives feed for"
    APP_STATE ||--o{ PREFERENCES : "combined with"
Loading

πŸ› οΈ Tech Stack

Frontend

  • React 18 - UI framework with hooks
  • Vite - Build tool and dev server
  • React Router - Client-side routing
  • Tailwind CSS - Utility-first styling
  • Lucide React - Icon library
  • Framer Motion - Animations (optional)

Mobile

  • Capacitor - Cross-platform native runtime
  • Capacitor HTTP - Native HTTP requests
  • Android SDK - Android app compilation
  • Xcode - iOS app compilation

Backend/API

  • Google News RSS - News data source
  • XML Parser - RSS feed processing
  • CORS Proxy - Web development (allorigins.win)
  • Country API - Country-specific news feeds

Storage

  • localStorage - Client-side data persistence
  • No Database - Fully client-side application

πŸ“ Project Structure

newsly/
β”‚
β”œβ”€β”€ src/                              # React application source
β”‚   β”‚
β”‚   β”œβ”€β”€ App.jsx                       # Root component β€” React Router route definitions
β”‚   β”œβ”€β”€ main.jsx                      # Entry point β€” mounts React + global CSS
β”‚   β”œβ”€β”€ index.css                     # Tailwind base imports + custom global styles
β”‚   β”‚
β”‚   β”œβ”€β”€ pages/                        # Full-screen route pages
β”‚   β”‚   β”œβ”€β”€ Landing.jsx               # Hero onboarding β€” feature grid, dark/light toggle
β”‚   β”‚   β”œβ”€β”€ GenreSelection.jsx        # 2-col gradient tile grid, progress bar, theme toggle
β”‚   β”‚   β”œβ”€β”€ Feed.jsx                  # News feed β€” TikTok swipe stack, country selector
β”‚   β”‚   β”œβ”€β”€ Settings.jsx              # Full-page settings β€” sections, toggles, modals
β”‚   β”‚   └── Bookmarks.jsx             # Saved articles list
β”‚   β”‚
β”‚   β”œβ”€β”€ components/                   # Reusable UI building blocks
β”‚   β”‚   β”œβ”€β”€ NewsCard.jsx              # Article card β€” image pipeline, reading time, bias auto-run
β”‚   β”‚   β”œβ”€β”€ CommentsCard.jsx          # Inline comments β€” name prompt, 500-char limit, like dedup
β”‚   β”‚   β”œβ”€β”€ CountrySelector.jsx       # Flag dropdown (z-50, above card stack)
β”‚   β”‚   └── ErrorBoundary.jsx         # Class component β€” render error catch + retry UI
β”‚   β”‚
β”‚   β”œβ”€β”€ contexts/                     # React context providers
β”‚   β”‚   β”œβ”€β”€ ThemeContext.jsx          # isDark state, toggleTheme(), localStorage sync
β”‚   β”‚   └── BookmarkContext.jsx       # bookmarks[], add/remove, localStorage sync
β”‚   β”‚
β”‚   └── utils/                        # Pure helpers and service modules
β”‚       β”œβ”€β”€ api.js                    # fetchNews() β€” serverless-first, CORS proxy fallback
β”‚       β”œβ”€β”€ mockApi.js                # XML β†’ article[] parser Β· getCountryParam() export
β”‚       β”œβ”€β”€ genres.js                 # Genre definitions + GENRE_STYLES gradient map
β”‚       β”œβ”€β”€ storage.js                # get/set helpers Β· hashKey(str) djb2 export
β”‚       β”œβ”€β”€ constants.js              # PAYWALLED_DOMAINS (30), countries, flags
β”‚       β”œβ”€β”€ speech.js                 # SpeechManager singleton β€” TTS read-aloud
β”‚       └── __tests__/
β”‚           └── countryNews.test.js   # Jest unit tests for country news fetching
β”‚
β”œβ”€β”€ api/                              # Vercel serverless functions
β”‚   β”œβ”€β”€ news.js                       # /api/news β€” server-side RSS fetch (CORS bypass)
β”‚   └── ai/
β”‚       └── bias-analysis.js          # /api/ai/bias-analysis β€” AI bias scoring endpoint
β”‚
β”œβ”€β”€ android/                          # Capacitor Android project (auto-generated)
β”‚   β”œβ”€β”€ app/
β”‚   β”‚   β”œβ”€β”€ src/main/
β”‚   β”‚   β”‚   β”œβ”€β”€ AndroidManifest.xml   # Permissions: internet, vibrate
β”‚   β”‚   β”‚   β”œβ”€β”€ java/com/newsly/app/
β”‚   β”‚   β”‚   β”‚   └── MainActivity.java # Capacitor bridge entry point
β”‚   β”‚   β”‚   └── res/                  # Icons, splash screens, layouts
β”‚   β”‚   └── build.gradle
β”‚   β”œβ”€β”€ capacitor.build.gradle
β”‚   β”œβ”€β”€ variables.gradle              # SDK version pins
β”‚   └── local.properties              # sdk.dir path (machine-local, gitignored)
β”‚
β”œβ”€β”€ index.html                        # Vite HTML shell
β”œβ”€β”€ vite.config.js                    # Vite build config
β”œβ”€β”€ tailwind.config.js                # Tailwind theme + dark mode: 'class'
β”œβ”€β”€ postcss.config.js                 # PostCSS (Tailwind + Autoprefixer)
β”œβ”€β”€ capacitor.config.json             # App ID, webDir, CapacitorHttp plugin config
β”œβ”€β”€ jest.config.js                    # Jest test runner config
β”œβ”€β”€ vercel.json                       # Vercel routing / function config
└── package.json                      # Scripts, dependencies

πŸ”§ API Architecture

News Fetching Strategy

// Platform Detection
if (window.Capacitor?.isNativePlatform()) {
  // Native: Use Capacitor HTTP (bypasses CORS)
  const response = await CapacitorHttp.get({
    url: googleNewsRssUrl,
    headers: { 'User-Agent': 'NewsBot/1.0' }
  });
} else {
  // Web: Use CORS proxy
  const response = await fetch(corsProxyUrl + googleNewsRssUrl);
}

Country-Specific RSS URLs

const getCountrySpecificUrl = (category, country) => {
  const baseUrls = {
    'technology': 'https://news.google.com/rss/topics/CAAqJggKIiBDQkFTRWdvSUwyMHZNRFZxYUdjU0FtVnVHZ0pWVXlnQVAB',
    'business': 'https://news.google.com/rss/topics/CAAqJggKIiBDQkFTRWdvSUwyMHZNRGx6TVdZU0FtVnVHZ0pWVXlnQVAB',
    // ... more categories
  };

  return country === 'global'
    ? baseUrls[category]
    : `${baseUrls[category]}?hl=${country}&gl=${country.toUpperCase()}`;
};

RSS Feed Processing

// XML to JSON conversion with country support
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
const items = xmlDoc.querySelectorAll('item');

// Extract article data
const articles = Array.from(items).map(item => ({
  title: item.querySelector('title')?.textContent,
  description: cleanDescription(item.querySelector('description')?.textContent),
  link: item.querySelector('link')?.textContent,
  pubDate: new Date(item.querySelector('pubDate')?.textContent),
  category: category,
  country: selectedCountry,
  id: generateUniqueId()
}));

πŸš€ Getting Started

Prerequisites

  • Node.js 18+ (Current: v20.16.0 supported)
  • Android Studio (for Android development)
  • Xcode (for iOS development, macOS only)

Installation

  1. Clone the repository:
git clone <your-repo-url>
cd newsly
  1. Install dependencies:
npm install
  1. Start development server:
npm run dev
  1. Open in browser:
http://localhost:5173

Mobile Development

Android Setup

  1. Add Android platform:
npx cap add android
  1. Build and sync:
npm run build
npx cap sync
  1. Open in Android Studio:
npx cap open android
  1. Run on device/emulator:
npm run android

iOS Setup (macOS only)

  1. Add iOS platform:
npx cap add ios
  1. Build and sync:
npm run build
npx cap sync
  1. Open in Xcode:
npx cap open ios

πŸ“± Building for Production

Web Deployment

# Build for web
npm run build

# Preview build
npm run preview

# Deploy to Vercel/Netlify
# Upload dist/ folder

Android APK

  1. Generate keystore:
keytool -genkey -v -keystore my-release-key.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias
  1. Build signed APK:
npm run build
npx cap sync
npx cap build android --keystorepath ./my-release-key.keystore --keystorepass YOUR_PASSWORD --keystorealias my-key-alias --keystorealiaspass YOUR_ALIAS_PASSWORD --androidreleasetype APK
  1. APK location:
android/app/build/outputs/apk/release/app-release-signed.apk

iOS App Store

  1. Build for iOS:
npm run build
npx cap sync
npx cap open ios
  1. In Xcode:
    • Set signing team
    • Archive for distribution
    • Upload to App Store Connect

πŸ”§ Configuration

Capacitor Config

{
  "appId": "com.newsly.app",
  "appName": "Newsly",
  "webDir": "dist",
  "server": {
    "androidScheme": "https",
    "cleartext": true,
    "allowNavigation": ["*"]
  },
  "plugins": {
    "CapacitorHttp": {
      "enabled": true
    }
  }
}

Environment Variables

# .env.local (optional, for future API keys)
NEWS_API_KEY=your_api_key_here
VITE_APP_NAME=Newsly

🎨 Customization

Adding New Countries

  1. Update countries.js:
export const countries = [
  // ... existing countries
  { code: 'de', name: 'Germany', flag: 'πŸ‡©πŸ‡ͺ' }
];
  1. Country will auto-work with existing RSS feeds

Adding New Categories

  1. Update genres.js:
export const genres = [
  // ... existing genres
  { id: 'science', name: 'Science', icon: 'πŸ”¬', color: 'bg-green-500' }
];
  1. Add RSS URL in api.js:
const categoryUrls = {
  // ... existing URLs
  'science': 'https://news.google.com/rss/topics/SCIENCE_RSS_URL'
};

Theming

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: '#your-color',
        secondary: '#your-color'
      }
    }
  }
}

πŸ§ͺ Testing

# Run linting
npm run lint

# Fix linting issues
npm run lint:fix

# Test on different devices
npm run android  # Android emulator
npm run ios      # iOS simulator (macOS)
npm run dev      # Web browser

πŸ“Š Performance

  • Bundle Size: ~500KB (gzipped)
  • First Load: <2s on 3G
  • News Fetch: <1s average
  • Country Switch: <500ms
  • Offline Support: Cached articles available
  • Memory Usage: <50MB on mobile

πŸ”’ Security

  • No API Keys: Uses public RSS feeds
  • HTTPS Only: All requests encrypted
  • No User Data: Everything stored locally
  • CORS Handled: Proper cross-origin setup
  • Content Security: Sanitized HTML content

πŸš€ Deployment Options

Web Hosting

  • Vercel (Recommended)
  • Netlify
  • GitHub Pages
  • Firebase Hosting

Mobile Distribution

  • Google Play Store
  • Apple App Store
  • Direct APK/IPA distribution
  • Enterprise deployment

🀝 Contributing

  1. Fork the repository
  2. Create feature branch (git checkout -b feature/amazing-feature)
  3. Commit changes (git commit -m 'Add amazing feature')
  4. Push to branch (git push origin feature/amazing-feature)
  5. Open Pull Request

πŸ“ License

MIT License - see LICENSE file for details.

πŸ™ Acknowledgments

  • Google News - RSS feed data source
  • Capacitor - Cross-platform framework
  • React Team - UI framework
  • Tailwind CSS - Styling system
  • Lucide - Icon library

Built with ❀️ by V

Newsly - Stay informed, stay brief.

About

"Ultra-short news android app built with React and Capacitor"

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors