From 4a4cb3ad0169c17396a472e2a604d9213596ac8c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Feb 2026 17:32:07 +0000 Subject: [PATCH 01/23] feat: configurable news reader with 12 categories, preferences, email digests Major overhaul of AI-Pulse into a fully configurable, personalized news reader: - config.json: externalized 12 categories (AI, Cybersecurity, IoT, Windows, Mac, Linux, Tech, Entrepreneurship, Finance, Crypto, Open Source, Products) with 112 RSS sources in FR/EN, bilingual labels, and keyword mappings - aggregator.js: config-driven, franc-min language detection, article deduplication (70% Dice similarity), local Readability reader, RSS feed generation (global + per-category), email digests via Resend API - readme-viewer.html: section navigation sidebar with scroll spy, preferences panel (language, categories, keywords, article count slider), DOM-based filtering - reader.html: back button in article info bar - tracker.js: preferences manager, read history, bookmarks manager (localStorage) - All pages: portfolio moved from nav to footer, added "Proposer une source" and "S'abonner" links in footer pointing to GitHub Issue templates - Issue templates: source submission (new-source.yml) and subscription (subscribe.yml) - Workflows: add-source.yml (auto-add approved sources), manage-subscriber.yml (auto-add subscribers), update-ai-pulse.yml (API_RESEND env var) - Email digest: HTML template with personalized content per subscriber preferences https://claude.ai/code/session_0138bAjho1fWwiRZju3nJFJ3 --- .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/new-source.yml | 65 ++ .github/ISSUE_TEMPLATE/subscribe.yml | 48 ++ .github/workflows/add-source.yml | 122 ++++ .github/workflows/manage-subscriber.yml | 122 ++++ .github/workflows/update-ai-pulse.yml | 1 + about.html | 7 +- app.html | 7 +- config.json | 262 ++++++++ data/subscribers.json | 1 + index.html | 9 +- js/tracker.js | 133 +++- package-lock.json | 48 ++ package.json | 3 +- privacy.html | 7 +- reader.html | 13 +- readme-viewer.html | 781 ++++++++++++++++++------ src/aggregator.js | 432 +++++++++---- stats.html | 7 +- templates/email-digest.html | 41 ++ 20 files changed, 1777 insertions(+), 337 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/new-source.yml create mode 100644 .github/ISSUE_TEMPLATE/subscribe.yml create mode 100644 .github/workflows/add-source.yml create mode 100644 .github/workflows/manage-subscriber.yml create mode 100644 config.json create mode 100644 data/subscribers.json create mode 100644 templates/email-digest.html diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..7793a8879 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: AI-Pulse Reader + url: https://thephoenixagency.github.io/AI-Pulse/app.html + about: Accedez au reader en ligne diff --git a/.github/ISSUE_TEMPLATE/new-source.yml b/.github/ISSUE_TEMPLATE/new-source.yml new file mode 100644 index 000000000..e89574b7e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-source.yml @@ -0,0 +1,65 @@ +name: Proposer une nouvelle source / Suggest a new source +description: Proposez une source RSS a ajouter a AI-Pulse +title: "[SOURCE] " +labels: ["new-source"] +body: + - type: input + id: source_name + attributes: + label: Nom de la source / Source Name + placeholder: "Ex: Korben, TechCrunch..." + validations: + required: true + - type: input + id: source_url + attributes: + label: URL du site / Website URL + placeholder: "https://example.com" + validations: + required: true + - type: input + id: rss_url + attributes: + label: URL du flux RSS / RSS Feed URL + placeholder: "https://example.com/feed/" + validations: + required: true + - type: dropdown + id: category + attributes: + label: Categorie / Category + options: + - AI / IA + - Cybersecurity / Cybersecurite + - IoT + - Windows + - Mac / Apple + - Linux + - Tech Generale + - Entrepreneuriat / Entrepreneurship + - Bourse & Finance + - Crypto & Blockchain + - Open Source & GitHub + - Produits & Innovation + validations: + required: true + - type: dropdown + id: language + attributes: + label: Langue / Language + options: + - Francais / French + - Anglais / English + - Autre / Other + validations: + required: true + - type: input + id: tags + attributes: + label: Tags / Mots-cles + placeholder: "Ex: tech, security, AI..." + - type: textarea + id: reason + attributes: + label: Pourquoi cette source? / Why this source? + placeholder: "Contenu original, source fiable..." diff --git a/.github/ISSUE_TEMPLATE/subscribe.yml b/.github/ISSUE_TEMPLATE/subscribe.yml new file mode 100644 index 000000000..2177d7cd6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/subscribe.yml @@ -0,0 +1,48 @@ +name: S'abonner a AI-Pulse / Subscribe to AI-Pulse +description: Recevez votre digest personnalise par email +title: "[SUBSCRIBE] " +labels: ["subscription"] +body: + - type: input + id: email + attributes: + label: Votre email / Your email + placeholder: "you@example.com" + validations: + required: true + - type: checkboxes + id: categories + attributes: + label: Categories souhaitees / Desired categories + options: + - label: AI / IA + - label: Cybersecurity + - label: IoT + - label: Windows + - label: Mac / Apple + - label: Linux + - label: Tech Generale + - label: Entrepreneuriat + - label: Bourse & Finance + - label: Crypto & Blockchain + - label: Open Source & GitHub + - label: Produits & Innovation + - type: dropdown + id: language + attributes: + label: Langue preferee / Preferred language + options: + - Francais + - English + - Les deux / Both + validations: + required: true + - type: dropdown + id: frequency + attributes: + label: Frequence / Frequency + options: + - Chaque mise a jour (toutes les 3h) / Every update + - Quotidien / Daily digest + validations: + required: true diff --git a/.github/workflows/add-source.yml b/.github/workflows/add-source.yml new file mode 100644 index 000000000..01d08173b --- /dev/null +++ b/.github/workflows/add-source.yml @@ -0,0 +1,122 @@ +name: Auto-add approved source + +on: + issues: + types: [labeled] + +permissions: + contents: write + issues: write + +jobs: + add-source: + if: github.event.label.name == 'source-approved' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Parse issue and add source + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + const body = issue.body; + + // Parse form fields from issue body + function getField(label) { + const regex = new RegExp(`### ${label}\\s*\\n\\s*(.+)`, 'i'); + const match = body.match(regex); + return match ? match[1].trim() : ''; + } + + const sourceName = getField('Nom de la source / Source Name'); + const sourceUrl = getField('URL du site / Website URL'); + const rssUrl = getField('URL du flux RSS / RSS Feed URL'); + const categoryRaw = getField('Categorie / Category'); + const languageRaw = getField('Langue / Language'); + const tags = getField('Tags / Mots-cles'); + + if (!sourceName || !rssUrl) { + console.log('Missing required fields'); + return; + } + + // Map category + const catMap = { + 'AI / IA': 'ai', + 'Cybersecurity / Cybersecurite': 'cybersecurity', + 'IoT': 'iot', + 'Windows': 'windows', + 'Mac / Apple': 'mac', + 'Linux': 'linux', + 'Tech Generale': 'tech', + 'Entrepreneuriat / Entrepreneurship': 'entrepreneurship', + 'Bourse & Finance': 'finance', + 'Crypto & Blockchain': 'crypto', + 'Open Source & GitHub': 'opensource', + 'Produits & Innovation': 'products' + }; + const category = catMap[categoryRaw] || 'tech'; + + // Map language + const lang = languageRaw.includes('Francais') ? 'fr' : 'en'; + + // Parse tags + const tagList = tags ? tags.split(',').map(t => t.trim()).filter(t => t) : [category]; + + // Read and update config.json + const config = JSON.parse(fs.readFileSync('config.json', 'utf-8')); + if (!config.categories[category]) { + console.log(`Category ${category} not found`); + return; + } + + // Check for duplicates + const exists = config.categories[category].feeds.some(f => f.url === rssUrl); + if (exists) { + console.log('Source already exists'); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `This source (${rssUrl}) already exists in the ${category} category.` + }); + return; + } + + // Add new feed + config.categories[category].feeds.push({ + name: sourceName, + url: rssUrl, + tags: tagList, + lang: lang + }); + + fs.writeFileSync('config.json', JSON.stringify(config, null, 2)); + + // Comment on issue + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `Source "${sourceName}" added to category "${category}" in config.json. It will be active at the next aggregation cycle.` + }); + + // Close issue + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed' + }); + + - name: Commit changes + run: | + git config --global user.name 'PhoenixProject-AutoSync' + git config --global user.email '${{ secrets.GIT_AUTHOR_EMAIL }}' + git add config.json + if ! git diff --cached --exit-code; then + git commit -m "Add new source from issue #${{ github.event.issue.number }}" + git push + fi diff --git a/.github/workflows/manage-subscriber.yml b/.github/workflows/manage-subscriber.yml new file mode 100644 index 000000000..34ada499b --- /dev/null +++ b/.github/workflows/manage-subscriber.yml @@ -0,0 +1,122 @@ +name: Manage subscriber + +on: + issues: + types: [labeled] + +permissions: + contents: write + issues: write + +jobs: + add-subscriber: + if: github.event.label.name == 'subscription' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Parse and add subscriber + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const issue = context.payload.issue; + const body = issue.body; + + function getField(label) { + const regex = new RegExp(`### ${label}\\s*\\n\\s*(.+)`, 'i'); + const match = body.match(regex); + return match ? match[1].trim() : ''; + } + + function getCheckboxes(label) { + const regex = new RegExp(`### ${label}\\s*\\n([\\s\\S]*?)(?=###|$)`, 'i'); + const match = body.match(regex); + if (!match) return []; + return [...match[1].matchAll(/- \[X\] (.+)/gi)].map(m => m[1].trim()); + } + + const email = getField('Votre email / Your email'); + if (!email || !email.includes('@')) { + console.log('Invalid email'); + return; + } + + const selectedCats = getCheckboxes('Categories souhaitees / Desired categories'); + const langRaw = getField('Langue preferee / Preferred language'); + const freqRaw = getField('Frequence / Frequency'); + + const catMap = { + 'AI / IA': 'ai', + 'Cybersecurity': 'cybersecurity', + 'IoT': 'iot', + 'Windows': 'windows', + 'Mac / Apple': 'mac', + 'Linux': 'linux', + 'Tech Generale': 'tech', + 'Entrepreneuriat': 'entrepreneurship', + 'Bourse & Finance': 'finance', + 'Crypto & Blockchain': 'crypto', + 'Open Source & GitHub': 'opensource', + 'Produits & Innovation': 'products' + }; + + const categories = selectedCats.map(c => catMap[c]).filter(Boolean); + const lang = langRaw.includes('Francais') ? 'fr' : langRaw.includes('English') ? 'en' : 'all'; + const frequency = freqRaw.includes('Quotidien') ? 'daily' : 'each_update'; + + // Read subscribers + const subsPath = 'data/subscribers.json'; + let subscribers = []; + try { + subscribers = JSON.parse(fs.readFileSync(subsPath, 'utf-8')); + } catch (e) { + subscribers = []; + } + + // Check duplicate + const exists = subscribers.some(s => s.email === email); + if (exists) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `This email is already subscribed. To update preferences, please unsubscribe first and resubscribe.` + }); + return; + } + + // Add subscriber + subscribers.push({ + email, + categories: categories.length > 0 ? categories : Object.values(catMap), + lang, + frequency, + subscribed_at: new Date().toISOString().split('T')[0] + }); + + fs.writeFileSync(subsPath, JSON.stringify(subscribers, null, 2)); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: `Welcome! You've been subscribed to AI-Pulse digest.\n\nCategories: ${categories.join(', ') || 'All'}\nLanguage: ${lang}\nFrequency: ${frequency}\n\nYou will receive your personalized digest at your next update cycle.` + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed' + }); + + - name: Commit changes + run: | + git config --global user.name 'PhoenixProject-AutoSync' + git config --global user.email '${{ secrets.GIT_AUTHOR_EMAIL }}' + git add data/subscribers.json + if ! git diff --cached --exit-code; then + git commit -m "Add subscriber from issue #${{ github.event.issue.number }}" + git push + fi diff --git a/.github/workflows/update-ai-pulse.yml b/.github/workflows/update-ai-pulse.yml index f6dabff99..db8732ee5 100644 --- a/.github/workflows/update-ai-pulse.yml +++ b/.github/workflows/update-ai-pulse.yml @@ -77,6 +77,7 @@ jobs: LINKEDIN_ACCESS_TOKEN: ${{ secrets.LINKEDIN_ACCESS_TOKEN }} LINKEDIN_USER_ID: ${{ secrets.LINKEDIN_USER_ID }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + API_RESEND: ${{ secrets.API_RESEND }} run: | node src/aggregator.js > NEW-README.md diff --git a/about.html b/about.html index 9b5cc7bd1..8de229a9f 100644 --- a/about.html +++ b/about.html @@ -68,7 +68,6 @@ @@ -143,9 +142,15 @@

Privacy First

Statistics + Portfolio + ThePhoenixAgency GitHub + + Proposer une source + + S'abonner

© 2026 AI-Pulse. All rights reserved.

diff --git a/app.html b/app.html index d7c4d29ce..4ff042708 100644 --- a/app.html +++ b/app.html @@ -36,7 +36,6 @@ @@ -51,9 +50,15 @@ Statistics + Portfolio + ThePhoenixAgency GitHub + + Proposer une source + + S'abonner

© 2026 AI-Pulse. All rights reserved.

diff --git a/config.json b/config.json new file mode 100644 index 000000000..66374b677 --- /dev/null +++ b/config.json @@ -0,0 +1,262 @@ +{ + "settings": { + "articlesPerFeed": 15, + "maxArticlesPerCategory": 30, + "summaryMaxLength": 600, + "deduplication": { + "enabled": true, + "similarityThreshold": 0.7 + } + }, + "categories": { + "ai": { + "labels": { "en": "AI - Artificial Intelligence", "fr": "IA - Intelligence Artificielle" }, + "icon": "brain", + "priority": 1, + "feeds": [ + { "name": "TechCrunch AI", "url": "https://techcrunch.com/category/artificial-intelligence/feed/", "tags": ["AI", "Startups", "Tech"], "lang": "en" }, + { "name": "VentureBeat AI", "url": "https://venturebeat.com/category/ai/feed/", "tags": ["AI", "Enterprise", "Business"], "lang": "en" }, + { "name": "AI News", "url": "https://www.artificialintelligence-news.com/feed/", "tags": ["AI", "News", "Industry"], "lang": "en" }, + { "name": "Google AI Blog", "url": "https://ai.googleblog.com/feeds/posts/default", "tags": ["AI", "Research", "Google"], "lang": "en" }, + { "name": "MIT Technology Review", "url": "https://www.technologyreview.com/feed/", "tags": ["AI", "Research", "Innovation"], "lang": "en" }, + { "name": "OpenAI Blog", "url": "https://openai.com/blog/rss.xml", "tags": ["AI", "LLM", "GPT"], "lang": "en" }, + { "name": "Hugging Face Blog", "url": "https://huggingface.co/blog/feed.xml", "tags": ["AI", "ML", "Open Source"], "lang": "en" }, + { "name": "Medium AI", "url": "https://medium.com/tag/artificial-intelligence/feed", "tags": ["AI", "ML", "Deep Learning"], "lang": "en" }, + { "name": "Towards Data Science", "url": "https://towardsdatascience.com/feed", "tags": ["AI", "Data Science", "Analytics"], "lang": "en" }, + { "name": "ActuIA", "url": "https://actuia.com/feed/", "tags": ["IA", "Actualites", "France"], "lang": "fr" }, + { "name": "LeMagIT - IA", "url": "https://www.lemagit.fr/rss/IA.xml", "tags": ["IA", "Entreprise", "Tech"], "lang": "fr" }, + { "name": "Siecle Digital", "url": "https://siecledigital.fr/feed/", "tags": ["IA", "Digital", "Tech"], "lang": "fr" }, + { "name": "Numerama Tech", "url": "https://www.numerama.com/feed/", "tags": ["IA", "Tech", "Science"], "lang": "fr" } + ] + }, + "cybersecurity": { + "labels": { "en": "Cybersecurity", "fr": "Cybersecurite" }, + "icon": "shield", + "priority": 2, + "feeds": [ + { "name": "The Hacker News", "url": "https://feeds.feedburner.com/TheHackersNews", "tags": ["Security", "Vulnerabilities", "Threats"], "lang": "en" }, + { "name": "Bleeping Computer", "url": "https://www.bleepingcomputer.com/feed/", "tags": ["Security", "Malware", "CVE"], "lang": "en" }, + { "name": "Krebs on Security", "url": "https://krebsonsecurity.com/feed/", "tags": ["Security", "Fraud", "Privacy"], "lang": "en" }, + { "name": "Dark Reading", "url": "https://www.darkreading.com/rss_simple.asp", "tags": ["Security", "CVE", "Enterprise"], "lang": "en" }, + { "name": "SecurityWeek", "url": "https://www.securityweek.com/feed/", "tags": ["Security", "CVE", "News"], "lang": "en" }, + { "name": "Threatpost", "url": "https://threatpost.com/feed/", "tags": ["Security", "Threats", "CVE"], "lang": "en" }, + { "name": "Penta Security", "url": "https://www.pentasecurity.com/blog/feed/", "tags": ["Security", "WAF", "Encryption"], "lang": "en" }, + { "name": "Google Project Zero", "url": "https://googleprojectzero.blogspot.com/feeds/posts/default", "tags": ["Security", "0day", "Research"], "lang": "en" }, + { "name": "PortSwigger Research", "url": "https://portswigger.net/research/rss", "tags": ["Security", "WebSec", "Research"], "lang": "en" }, + { "name": "HackerOne Blog", "url": "https://www.hackerone.com/blog.rss", "tags": ["Bug Bounty", "Security", "Hacking"], "lang": "en" }, + { "name": "Bugcrowd Blog", "url": "https://www.bugcrowd.com/blog/feed", "tags": ["Bug Bounty", "Security", "Pentesting"], "lang": "en" }, + { "name": "Packet Storm Security", "url": "https://packetstormsecurity.com/feeds/", "tags": ["Exploits", "Advisories", "Security"], "lang": "en" }, + { "name": "Exploit-DB", "url": "https://www.exploit-db.com/rss.xml", "tags": ["Exploits", "CVE", "Security"], "lang": "en" }, + { "name": "CTFtime", "url": "https://ctftime.org/event/list/upcoming/rss/", "tags": ["CTF", "Hacking", "Competition"], "lang": "en" }, + { "name": "CERT-FR", "url": "https://www.cert.ssi.gouv.fr/feed/", "tags": ["Securite", "Alertes", "France"], "lang": "fr" }, + { "name": "Zataz", "url": "https://www.zataz.com/feed/", "tags": ["Securite", "Cybercrime", "France"], "lang": "fr" }, + { "name": "UnderNews", "url": "https://www.undernews.fr/feed", "tags": ["Securite", "Malware", "France"], "lang": "fr" }, + { "name": "NoLimitSecu", "url": "https://www.nolimitsecu.fr/feed/", "tags": ["Securite", "Podcast", "France"], "lang": "fr" }, + { "name": "LeMagIT - Securite", "url": "https://www.lemagit.fr/rss/Securite.xml", "tags": ["Securite", "Entreprise", "France"], "lang": "fr" } + ] + }, + "iot": { + "labels": { "en": "IoT - Internet of Things", "fr": "IdO - Internet des Objets" }, + "icon": "cpu", + "priority": 3, + "feeds": [ + { "name": "Raspberry Pi", "url": "https://www.raspberrypi.com/news/feed/", "tags": ["IoT", "RaspberryPi", "Hardware"], "lang": "en" }, + { "name": "Arduino Blog", "url": "https://blog.arduino.cc/feed/", "tags": ["IoT", "Arduino", "Hardware"], "lang": "en" }, + { "name": "Hackster.io", "url": "https://www.hackster.io/news/feed", "tags": ["IoT", "Hardware", "Projects"], "lang": "en" }, + { "name": "IoT For All", "url": "https://www.iotforall.com/feed", "tags": ["IoT", "News"], "lang": "en" }, + { "name": "IoT Business News", "url": "https://iotbusinessnews.com/feed/", "tags": ["IoT", "Business", "News"], "lang": "en" }, + { "name": "IoT World Today", "url": "https://www.iotworldtoday.com/feed", "tags": ["IoT", "News"], "lang": "en" }, + { "name": "Adafruit Blog", "url": "https://blog.adafruit.com/feed/", "tags": ["IoT", "Maker", "Hardware"], "lang": "en" }, + { "name": "SparkFun Blog", "url": "https://www.sparkfun.com/news/feed", "tags": ["IoT", "Electronics", "Hardware"], "lang": "en" }, + { "name": "Embedded.com", "url": "https://www.embedded.com/feed", "tags": ["Embedded", "IoT", "Hardware"], "lang": "en" }, + { "name": "Domotique News", "url": "http://www.domotique-news.com/feed", "tags": ["Domotique", "IoT", "Maison"], "lang": "fr" }, + { "name": "Framboise 314", "url": "https://www.framboise314.fr/feed/", "tags": ["RaspberryPi", "IoT", "DIY"], "lang": "fr" }, + { "name": "Maison et Domotique", "url": "https://www.maison-et-domotique.com/feed/", "tags": ["Domotique", "SmartHome", "IoT"], "lang": "fr" } + ] + }, + "windows": { + "labels": { "en": "Windows", "fr": "Windows" }, + "icon": "monitor", + "priority": 4, + "feeds": [ + { "name": "Windows Central", "url": "https://www.windowscentral.com/feed", "tags": ["Windows", "Microsoft", "PC"], "lang": "en" }, + { "name": "Neowin", "url": "https://www.neowin.net/news/rss/", "tags": ["Windows", "Microsoft", "Tech"], "lang": "en" }, + { "name": "Windows Latest", "url": "https://www.windowslatest.com/feed/", "tags": ["Windows", "Updates", "Microsoft"], "lang": "en" }, + { "name": "BleepingComputer Windows", "url": "https://www.bleepingcomputer.com/feed/", "tags": ["Windows", "Security", "Updates"], "lang": "en" }, + { "name": "Next INpact", "url": "https://next.ink/feed/", "tags": ["Windows", "Tech", "Numerique"], "lang": "fr" }, + { "name": "GNT", "url": "https://www.generation-nt.com/export/rss.xml", "tags": ["Windows", "Tech", "Logiciels"], "lang": "fr" } + ] + }, + "mac": { + "labels": { "en": "Mac / Apple", "fr": "Mac / Apple" }, + "icon": "apple", + "priority": 5, + "feeds": [ + { "name": "9to5Mac", "url": "https://9to5mac.com/feed/", "tags": ["Apple", "Mac", "iOS"], "lang": "en" }, + { "name": "MacRumors", "url": "https://feeds.macrumors.com/MacRumors-All", "tags": ["Apple", "Mac", "Rumors"], "lang": "en" }, + { "name": "AppleInsider", "url": "https://appleinsider.com/rss/news/", "tags": ["Apple", "Mac", "News"], "lang": "en" }, + { "name": "The Mac Observer", "url": "https://www.macobserver.com/feed/", "tags": ["Apple", "Mac", "Reviews"], "lang": "en" }, + { "name": "MacGeneration", "url": "https://www.macgeneration.com/rss", "tags": ["Apple", "Mac", "France"], "lang": "fr" }, + { "name": "iGeneration", "url": "https://www.igen.fr/rss", "tags": ["Apple", "iOS", "France"], "lang": "fr" } + ] + }, + "linux": { + "labels": { "en": "Linux", "fr": "Linux" }, + "icon": "terminal", + "priority": 6, + "feeds": [ + { "name": "OMG! Ubuntu", "url": "https://www.omgubuntu.co.uk/feed", "tags": ["Linux", "Ubuntu", "Open Source"], "lang": "en" }, + { "name": "Its FOSS", "url": "https://itsfoss.com/feed/", "tags": ["Linux", "Open Source", "Tutorials"], "lang": "en" }, + { "name": "Phoronix", "url": "https://www.phoronix.com/rss.php", "tags": ["Linux", "Benchmarks", "Hardware"], "lang": "en" }, + { "name": "LWN.net", "url": "https://lwn.net/headlines/rss", "tags": ["Linux", "Kernel", "Development"], "lang": "en" }, + { "name": "GamingOnLinux", "url": "https://www.gamingonlinux.com/article_rss.php", "tags": ["Linux", "Gaming", "Proton"], "lang": "en" }, + { "name": "LinuxFr", "url": "https://linuxfr.org/news.atom", "tags": ["Linux", "Logiciel Libre", "France"], "lang": "fr" }, + { "name": "Journal du Hacker", "url": "https://www.journalduhacker.net/rss", "tags": ["Linux", "Hacking", "Open Source"], "lang": "fr" }, + { "name": "Toolinux", "url": "https://www.toolinux.com/feed", "tags": ["Linux", "Logiciel Libre", "France"], "lang": "fr" } + ] + }, + "tech": { + "labels": { "en": "General Tech", "fr": "Tech Generale" }, + "icon": "zap", + "priority": 7, + "feeds": [ + { "name": "The Verge", "url": "https://www.theverge.com/rss/index.xml", "tags": ["Tech", "News", "Reviews"], "lang": "en" }, + { "name": "Ars Technica", "url": "https://feeds.arstechnica.com/arstechnica/index", "tags": ["Tech", "Science", "Analysis"], "lang": "en" }, + { "name": "Wired", "url": "https://www.wired.com/feed/rss", "tags": ["Tech", "Culture", "Science"], "lang": "en" }, + { "name": "TechRadar", "url": "https://www.techradar.com/rss", "tags": ["Tech", "Reviews", "Gadgets"], "lang": "en" }, + { "name": "Korben", "url": "https://korben.info/feed", "tags": ["Tech", "Outils", "Astuces"], "lang": "fr" }, + { "name": "Frandroid", "url": "https://www.frandroid.com/feed", "tags": ["Tech", "Mobile", "Android"], "lang": "fr" }, + { "name": "Numerama", "url": "https://www.numerama.com/feed/", "tags": ["Tech", "Numerique", "Societe"], "lang": "fr" }, + { "name": "Le Monde Informatique", "url": "https://www.lemondeinformatique.fr/flux-rss/thematique/toutes-les-actualites/rss/", "tags": ["Tech", "Entreprise", "IT"], "lang": "fr" } + ] + }, + "entrepreneurship": { + "labels": { "en": "Entrepreneurship", "fr": "Entrepreneuriat" }, + "icon": "rocket", + "priority": 8, + "feeds": [ + { "name": "TechCrunch Startups", "url": "https://techcrunch.com/category/startups/feed/", "tags": ["Startups", "Fundraising", "Tech"], "lang": "en" }, + { "name": "Entrepreneur.com", "url": "https://www.entrepreneur.com/latest/feed", "tags": ["Entrepreneurship", "Business", "Startups"], "lang": "en" }, + { "name": "Inc.com", "url": "https://www.inc.com/rss", "tags": ["Business", "Startups", "Leadership"], "lang": "en" }, + { "name": "Maddyness", "url": "https://www.maddyness.com/feed/", "tags": ["Startups", "Entrepreneuriat", "France"], "lang": "fr" }, + { "name": "FrenchWeb", "url": "https://www.frenchweb.fr/feed", "tags": ["Startups", "Tech", "France"], "lang": "fr" }, + { "name": "BPI France Le Hub", "url": "https://lehub.bpifrance.fr/feed/", "tags": ["Entrepreneuriat", "Innovation", "France"], "lang": "fr" }, + { "name": "Les Echos Entrepreneurs", "url": "https://business.lesechos.fr/feed/", "tags": ["Business", "Entrepreneurs", "France"], "lang": "fr" }, + { "name": "Journal du Net", "url": "https://www.journaldunet.com/rss/", "tags": ["Business", "Tech", "Economie"], "lang": "fr" }, + { "name": "Capital", "url": "https://www.capital.fr/feed", "tags": ["Business", "Economie", "France"], "lang": "fr" } + ] + }, + "finance": { + "labels": { "en": "Stock Market & Finance", "fr": "Bourse & Finance" }, + "icon": "trending-up", + "priority": 9, + "feeds": [ + { "name": "MarketWatch", "url": "https://feeds.marketwatch.com/marketwatch/topstories/", "tags": ["Finance", "Markets", "Stocks"], "lang": "en" }, + { "name": "Yahoo Finance", "url": "https://finance.yahoo.com/rss/", "tags": ["Finance", "Markets", "Economy"], "lang": "en" }, + { "name": "Reuters Business", "url": "https://www.reuters.com/business/finance/rss", "tags": ["Finance", "Business", "Global"], "lang": "en" }, + { "name": "Investing.com", "url": "https://www.investing.com/rss/news.rss", "tags": ["Finance", "Trading", "Markets"], "lang": "en" }, + { "name": "Zonebourse", "url": "https://www.zonebourse.com/rss/", "tags": ["Bourse", "Actions", "Finance"], "lang": "fr" }, + { "name": "Boursorama", "url": "https://www.boursorama.com/rss/actualites", "tags": ["Bourse", "Marches", "Finance"], "lang": "fr" }, + { "name": "ABC Bourse", "url": "https://www.abcbourse.com/rss/", "tags": ["Bourse", "Analyse", "Finance"], "lang": "fr" }, + { "name": "Investir Les Echos", "url": "https://investir.lesechos.fr/feed/", "tags": ["Investissement", "Bourse", "Finance"], "lang": "fr" } + ] + }, + "crypto": { + "labels": { "en": "Crypto & Blockchain", "fr": "Crypto & Blockchain" }, + "icon": "link", + "priority": 10, + "feeds": [ + { "name": "CoinDesk", "url": "https://www.coindesk.com/arc/outboundfeeds/rss/", "tags": ["Crypto", "Bitcoin", "Blockchain"], "lang": "en" }, + { "name": "CoinTelegraph", "url": "https://cointelegraph.com/rss", "tags": ["Crypto", "Blockchain", "DeFi"], "lang": "en" }, + { "name": "The Block", "url": "https://www.theblock.co/rss.xml", "tags": ["Crypto", "Blockchain", "Analysis"], "lang": "en" }, + { "name": "Decrypt", "url": "https://decrypt.co/feed", "tags": ["Crypto", "Web3", "NFT"], "lang": "en" }, + { "name": "Journal du Coin", "url": "https://journalducoin.com/feed/", "tags": ["Crypto", "Bitcoin", "France"], "lang": "fr" }, + { "name": "Cryptoast", "url": "https://cryptoast.fr/feed/", "tags": ["Crypto", "Blockchain", "France"], "lang": "fr" }, + { "name": "CoinTribune", "url": "https://www.cointribune.com/feed/", "tags": ["Crypto", "Blockchain", "France"], "lang": "fr" } + ] + }, + "opensource": { + "labels": { "en": "Open Source & GitHub", "fr": "Open Source & GitHub" }, + "icon": "git-branch", + "priority": 11, + "feeds": [ + { "name": "GitHub Blog", "url": "https://github.blog/feed/", "tags": ["GitHub", "Open Source", "DevTools"], "lang": "en" }, + { "name": "GitHub Changelog", "url": "https://github.blog/changelog/feed/", "tags": ["GitHub", "Updates", "Features"], "lang": "en" }, + { "name": "GitHub Trending", "url": "https://mshibanami.github.io/GitHubTrendingRSS/daily/all.xml", "tags": ["GitHub", "Trending", "Repos"], "lang": "en" }, + { "name": "GitHub Trending Python", "url": "https://mshibanami.github.io/GitHubTrendingRSS/daily/python.xml", "tags": ["GitHub", "Python", "Trending"], "lang": "en" }, + { "name": "The Changelog", "url": "https://changelog.com/feed", "tags": ["Open Source", "Podcast", "DevTools"], "lang": "en" }, + { "name": "Console.dev", "url": "https://console.dev/rss.xml", "tags": ["DevTools", "Open Source", "Weekly"], "lang": "en" }, + { "name": "Dev.to Open Source", "url": "https://dev.to/feed/tag/opensource", "tags": ["Open Source", "Community", "Dev"], "lang": "en" }, + { "name": "LibHunt Selfhosted", "url": "https://selfhosted.libhunt.com/feed", "tags": ["Self-hosted", "Open Source", "Tools"], "lang": "en" } + ] + }, + "products": { + "labels": { "en": "Products & Innovation", "fr": "Produits & Innovation" }, + "icon": "package", + "priority": 12, + "feeds": [ + { "name": "Product Hunt", "url": "https://www.producthunt.com/feed", "tags": ["Products", "Launch", "Startups"], "lang": "en" }, + { "name": "Hacker News Show", "url": "https://hnrss.org/show", "tags": ["Products", "Launch", "HackerNews"], "lang": "en" }, + { "name": "BetaList", "url": "https://betalist.com/feed", "tags": ["Beta", "Startups", "Products"], "lang": "en" }, + { "name": "Indie Hackers", "url": "https://www.indiehackers.com/feed", "tags": ["Indie", "Products", "SaaS"], "lang": "en" }, + { "name": "Les Numeriques", "url": "https://www.lesnumeriques.com/rss.xml", "tags": ["Produits", "Tests", "Tech"], "lang": "fr" }, + { "name": "Journal du Geek", "url": "https://www.journaldugeek.com/feed/", "tags": ["Produits", "Gadgets", "Tech"], "lang": "fr" }, + { "name": "Futura Tech", "url": "https://www.futura-sciences.com/tech/rss", "tags": ["Innovation", "Science", "Tech"], "lang": "fr" }, + { "name": "01net", "url": "https://www.01net.com/rss/info/flux-rss.xml", "tags": ["Produits", "Tests", "Tech"], "lang": "fr" } + ] + } + }, + "keywords": { + "universal": [ + "LLM", "GPT", "ChatGPT", "Copilot", "Gemini", "Claude", "Mistral", "Llama", + "Stable Diffusion", "Midjourney", "Docker", "Kubernetes", "DevOps", + "GitHub", "GitLab", "Python", "Rust", "JavaScript", "TypeScript", + "Raspberry Pi", "Arduino", "ESP32", "Ubuntu", "Debian", "Fedora", "Arch", + "macOS", "iOS", "Android", "Windows 11", + "API", "Open Source", "Cloud", "AWS", "Azure", "GCP", + "Ransomware", "Phishing", "Zero-day", "CVE", "CERT", + "5G", "Wi-Fi", "Bluetooth", "Zigbee", "Matter", + "Tesla", "NVIDIA", "AMD", "Intel", "ARM", "RISC-V", + "OpenClaw", "ClowdBot", "MoltBot", + "Blockchain", "Bitcoin", "Ethereum", "DeFi", "NFT", "Web3", + "Startup", "Fintech", "SaaS", "Scaleup", + "CAC 40", "S&P 500", "NASDAQ", "Dow Jones", + "IPO", "Trading", + "Product Hunt", "Kickstarter", "Gadget", "Wearable", + "Self-hosted", "CLI", "Framework", "SDK" + ], + "localized": { + "artificial_intelligence": { "en": "Artificial Intelligence", "fr": "Intelligence Artificielle" }, + "machine_learning": { "en": "Machine Learning", "fr": "Apprentissage Automatique" }, + "deep_learning": { "en": "Deep Learning", "fr": "Apprentissage Profond" }, + "cybersecurity": { "en": "Cybersecurity", "fr": "Cybersecurite" }, + "data_breach": { "en": "Data Breach", "fr": "Fuite de Donnees" }, + "privacy": { "en": "Privacy", "fr": "Vie Privee" }, + "home_automation": { "en": "Home Automation", "fr": "Domotique" }, + "connected_objects": { "en": "Connected Objects", "fr": "Objets Connectes" }, + "open_source": { "en": "Open Source", "fr": "Logiciel Libre" }, + "update": { "en": "Update", "fr": "Mise a jour" }, + "vulnerability": { "en": "Vulnerability", "fr": "Vulnerabilite" }, + "neural_network": { "en": "Neural Network", "fr": "Reseau de Neurones" }, + "computer_vision": { "en": "Computer Vision", "fr": "Vision par Ordinateur" }, + "robotics": { "en": "Robotics", "fr": "Robotique" }, + "quantum_computing": { "en": "Quantum Computing", "fr": "Informatique Quantique" }, + "stock_market": { "en": "Stock Market", "fr": "Bourse" }, + "cryptocurrency": { "en": "Cryptocurrency", "fr": "Cryptomonnaie" }, + "fundraising": { "en": "Fundraising", "fr": "Levee de Fonds" }, + "entrepreneurship": { "en": "Entrepreneurship", "fr": "Entrepreneuriat" }, + "startup": { "en": "Startup", "fr": "Startup" }, + "investment": { "en": "Investment", "fr": "Investissement" }, + "bug_bounty": { "en": "Bug Bounty", "fr": "Bug Bounty" }, + "exploit": { "en": "Exploit", "fr": "Exploit" }, + "regulation": { "en": "Regulation", "fr": "Regulation" }, + "new_product": { "en": "New Product", "fr": "Nouveau Produit" }, + "launch": { "en": "Launch", "fr": "Lancement" }, + "crowdfunding": { "en": "Crowdfunding", "fr": "Financement Participatif" }, + "repository": { "en": "Repository", "fr": "Depot" }, + "self_hosted": { "en": "Self-hosted", "fr": "Auto-heberge" }, + "library": { "en": "Library", "fr": "Bibliotheque" }, + "tool": { "en": "Tool", "fr": "Outil" } + } + } +} diff --git a/data/subscribers.json b/data/subscribers.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/data/subscribers.json @@ -0,0 +1 @@ +[] diff --git a/index.html b/index.html index f9118e678..13f74503c 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,6 @@ @@ -95,11 +94,17 @@

Cybersecurity

Statistics + Portfolio + ThePhoenixAgency GitHub + + Proposer une source + + S'abonner -

© 2025 - AI-Pulse by PhoenixProject. All rights reserved.

+

© 2026 AI-Pulse. All rights reserved.

diff --git a/js/tracker.js b/js/tracker.js index 88bf29833..415b4d962 100644 --- a/js/tracker.js +++ b/js/tracker.js @@ -1,6 +1,6 @@ /** * AI-Pulse Statistics Tracker - * Handles local visitor tracking using localStorage. + * Handles local visitor tracking, preferences, read history, and bookmarks via localStorage. */ const Tracker = { @@ -59,7 +59,6 @@ const Tracker = { .then(data => { if (data.error) return; - const locationStr = `${data.city}, ${data.country_name}`; const locationObj = { city: data.city, country: data.country_name, @@ -85,6 +84,12 @@ const Tracker = { let stats = this.getStats(); stats.articleClicks = (stats.articleClicks || 0) + 1; localStorage.setItem('ai_pulse_stats', JSON.stringify(stats)); + + // Also add to read history + if (articleData.url) { + ReadHistory.markRead(articleData.url, articleData.title || 'Unknown'); + } + console.log("Article tracked:", articleData.title); }, @@ -103,4 +108,128 @@ const Tracker = { } }; +/** + * Preferences Manager - Shared across all pages + * Same localStorage key as readme-viewer.html for consistency + */ +const PrefsManager = { + STORAGE_KEY: 'ai_pulse_preferences', + + getDefaults: function () { + return { + lang: 'all', + categories: {}, + keywords: '', + maxArticles: 30 + }; + }, + + load: function () { + try { + var stored = localStorage.getItem(this.STORAGE_KEY); + if (stored) { + var parsed = JSON.parse(stored); + var defaults = this.getDefaults(); + for (var key in defaults) { + if (!(key in parsed)) parsed[key] = defaults[key]; + } + return parsed; + } + } catch (e) { /* ignore */ } + return this.getDefaults(); + }, + + save: function (prefs) { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(prefs)); + }, + + reset: function () { + localStorage.removeItem(this.STORAGE_KEY); + } +}; + +/** + * Read History - Track which articles have been read + */ +const ReadHistory = { + STORAGE_KEY: 'ai_pulse_read_articles', + + getAll: function () { + try { + return JSON.parse(localStorage.getItem(this.STORAGE_KEY)) || []; + } catch (e) { return []; } + }, + + isRead: function (url) { + return this.getAll().some(function (a) { return a.url === url; }); + }, + + markRead: function (url, title) { + var articles = this.getAll(); + if (!articles.some(function (a) { return a.url === url; })) { + articles.push({ url: url, title: title, readAt: Date.now() }); + // Keep max 500 entries to avoid localStorage bloat + if (articles.length > 500) { + articles = articles.slice(-500); + } + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(articles)); + } + }, + + getCount: function () { + return this.getAll().length; + }, + + clear: function () { + localStorage.removeItem(this.STORAGE_KEY); + } +}; + +/** + * Bookmarks Manager - Save and manage bookmarked articles + */ +const Bookmarks = { + STORAGE_KEY: 'ai_pulse_bookmarks', + + getAll: function () { + try { + return JSON.parse(localStorage.getItem(this.STORAGE_KEY)) || []; + } catch (e) { return []; } + }, + + isBookmarked: function (url) { + return this.getAll().some(function (b) { return b.url === url; }); + }, + + toggle: function (url, title, source) { + var bookmarks = this.getAll(); + var index = bookmarks.findIndex(function (b) { return b.url === url; }); + if (index >= 0) { + bookmarks.splice(index, 1); + } else { + bookmarks.push({ url: url, title: title || '', source: source || '', savedAt: Date.now() }); + } + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(bookmarks)); + return index < 0; // returns true if added, false if removed + }, + + remove: function (url) { + var bookmarks = this.getAll().filter(function (b) { return b.url !== url; }); + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(bookmarks)); + }, + + getCount: function () { + return this.getAll().length; + }, + + clear: function () { + localStorage.removeItem(this.STORAGE_KEY); + } +}; + +// Expose globally +window.PrefsManager = PrefsManager; +window.ReadHistory = ReadHistory; +window.Bookmarks = Bookmarks; + Tracker.init(); diff --git a/package-lock.json b/package-lock.json index 5cb0fd1f3..c134be8e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "cors": "^2.8.6", "dompurify": "^3.3.1", "express": "^5.2.1", + "franc-min": "^6.2.0", "isomorphic-dompurify": "^2.35.0", "jimp": "^1.6.0", "jsdom": "^27.4.0", @@ -1059,6 +1060,16 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1632,6 +1643,19 @@ "node": ">= 0.6" } }, + "node_modules/franc-min": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/franc-min/-/franc-min-6.2.0.tgz", + "integrity": "sha512-1uDIEUSlUZgvJa2AKYR/dmJC66v/PvGQ9mWfI9nOr/kPpMFyvswK0gPXOwpYJYiYD008PpHLkGfG58SPjQJFxw==", + "license": "MIT", + "dependencies": { + "trigram-utils": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -2122,6 +2146,16 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/n-gram": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/n-gram/-/n-gram-2.0.2.tgz", + "integrity": "sha512-S24aGsn+HLBxUGVAUFOwGpKs7LBcG4RudKU//eWzt/mQ97/NMKQxDWHyHx63UNWk/OOdihgmzoETn1tf5nQDzQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2913,6 +2947,20 @@ "node": ">=20" } }, + "node_modules/trigram-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/trigram-utils/-/trigram-utils-2.0.1.tgz", + "integrity": "sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ==", + "license": "MIT", + "dependencies": { + "collapse-white-space": "^2.0.0", + "n-gram": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/package.json b/package.json index 34d887617..34fbe4ba0 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "cors": "^2.8.6", "dompurify": "^3.3.1", "express": "^5.2.1", + "franc-min": "^6.2.0", "isomorphic-dompurify": "^2.35.0", "jimp": "^1.6.0", "jsdom": "^27.4.0", @@ -38,4 +39,4 @@ "engines": { "node": ">=20.0.0" } -} \ No newline at end of file +} diff --git a/privacy.html b/privacy.html index d0abfd32d..511169271 100644 --- a/privacy.html +++ b/privacy.html @@ -63,7 +63,6 @@ @@ -161,9 +160,15 @@

7. Contact Us

Statistics + Portfolio + ThePhoenixAgency GitHub + + Proposer une source + + S'abonner

© 2026 AI-Pulse. All rights reserved.

diff --git a/reader.html b/reader.html index b626dfc8a..7324b1097 100644 --- a/reader.html +++ b/reader.html @@ -296,10 +296,15 @@
+
From f0654846339bbf35a3471a5d90670a078848d687 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 18:54:37 +0000 Subject: [PATCH 07/23] refactor: remove unused quick-nav CSS, keep improved section styling - Remove quick-nav styles (not needed) - Keep improved section separators with cards and left border https://claude.ai/code/session_0138bAjho1fWwiRZju3nJFJ3 --- readme-viewer.html | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/readme-viewer.html b/readme-viewer.html index f2994f9b4..c7be87fb7 100644 --- a/readme-viewer.html +++ b/readme-viewer.html @@ -198,46 +198,6 @@ .prefs-reset:hover { background: rgba(239, 68, 68, 0.2); } - /* ─── Quick Navigation Bar ─── */ - .quick-nav { - display: flex; - flex-wrap: wrap; - gap: 10px; - padding: 15px 20px; - margin-bottom: 20px; - background: rgba(0, 217, 255, 0.05); - border: 1px solid rgba(0, 217, 255, 0.15); - border-radius: 10px; - position: sticky; - top: 0; - z-index: 50; - backdrop-filter: blur(10px); - } - - .quick-nav a { - display: inline-block; - padding: 6px 14px; - background: rgba(0, 217, 255, 0.1); - color: #00d9ff; - text-decoration: none; - border-radius: 20px; - font-size: 0.85rem; - font-weight: 500; - border: 1px solid rgba(0, 217, 255, 0.2); - transition: all 0.2s; - } - - .quick-nav a:hover { - background: rgba(0, 217, 255, 0.25); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 217, 255, 0.3); - } - - .quick-nav a.active { - background: #00d9ff; - color: #0a0e27; - } - /* ─── Content Area ─── */ .content-area { margin-left: 220px; From f256beb5273663e179653e2120078c9898e4ca31 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Feb 2026 18:59:20 +0000 Subject: [PATCH 08/23] feat: replace sidebar with top dropdown navigation - Remove left sidebar navigation - Add fixed top bar with dropdown menu for section navigation - Dropdown shows only filtered categories (or all if no filter) - Center content area without left margin - Keep elevator buttons for scroll up/down - Update responsive styles https://claude.ai/code/session_0138bAjho1fWwiRZju3nJFJ3 --- readme-viewer.html | 151 +++++++++++++++++++++------------------------ 1 file changed, 72 insertions(+), 79 deletions(-) diff --git a/readme-viewer.html b/readme-viewer.html index c7be87fb7..617272aee 100644 --- a/readme-viewer.html +++ b/readme-viewer.html @@ -21,50 +21,56 @@ .main-layout { display: flex; } - /* ─── Section Navigation Sidebar ─── */ - .section-nav { + /* ─── Top Navigation Bar ─── */ + .top-nav { position: fixed; - left: 0; top: 0; - width: 220px; - height: 100vh; + left: 0; + right: 0; + height: 50px; background: rgba(10, 14, 39, 0.95); - border-right: 1px solid rgba(0, 217, 255, 0.15); - padding: 15px 0; - overflow-y: auto; + border-bottom: 1px solid rgba(0, 217, 255, 0.15); + display: flex; + align-items: center; + justify-content: center; z-index: 100; backdrop-filter: blur(10px); + padding: 0 20px; + } + + .top-nav label { + color: #94a3b8; + font-size: 0.85rem; + margin-right: 10px; } - .section-nav h3 { + .top-nav select { + padding: 8px 14px; + background: rgba(0, 0, 0, 0.3); color: #00d9ff; - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 1.5px; - padding: 0 15px 10px; - border-bottom: 1px solid rgba(0, 217, 255, 0.15); - margin-bottom: 8px; + border: 1px solid rgba(0, 217, 255, 0.3); + border-radius: 8px; + font-size: 0.85rem; + font-family: inherit; + cursor: pointer; + min-width: 200px; } - .section-nav a { - display: block; - padding: 8px 15px; - color: #94a3b8; - text-decoration: none; - font-size: 0.82rem; - font-weight: 500; - transition: all 0.2s; - border-left: 3px solid transparent; + .top-nav select:hover { + border-color: #00d9ff; } - .section-nav a:hover { color: #00d9ff; background: rgba(0, 217, 255, 0.05); } + .top-nav select:focus { + outline: none; + border-color: #00d9ff; + } - .section-nav a.active { - color: #00d9ff; - background: rgba(0, 217, 255, 0.1); - border-left-color: #00d9ff; + .top-nav select option { + background: #0a0e27; + color: #e2e8f0; } + /* ─── Preferences Button ─── */ .prefs-toggle { position: fixed; @@ -200,8 +206,8 @@ /* ─── Content Area ─── */ .content-area { - margin-left: 220px; - padding: 20px 30px 60px; + margin: 0 auto; + padding: 70px 30px 60px; max-width: 960px; } @@ -393,8 +399,10 @@ /* ─── Responsive ─── */ @media (max-width: 768px) { - .section-nav { display: none; } - .content-area { margin-left: 0; padding: 15px; } + .top-nav { padding: 0 10px; } + .top-nav label { display: none; } + .top-nav select { min-width: 150px; font-size: 0.8rem; } + .content-area { padding: 60px 15px 60px; } .prefs-panel { width: 100%; right: -100%; } } @@ -404,10 +412,12 @@ - -