trendscout / server.js
ziggylott's picture
Deploy Trendscout v1 β€” trend intelligence tool
4278447
Raw
History Blame Contribute Delete
10.4 kB
const express = require('express');
const axios = require('axios');
const cors = require('cors');
require('dotenv').config();
const app = express();
app.use(cors());
// ── Embedded Frontend ──────────────────────────────────────────────
app.get('/', (req, res) => {
res.send(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Trendscout β€” Find Profitable Product Ideas Fast</title>
<style>
:root{--bg:#0f0f13;--card:#1a1a24;--accent:#7c3aed;--accent2:#a855f7;--text:#f1f1f5;--muted:#9ca3af;--green:#22c55e;--border:#2a2a3a}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh}
header{background:linear-gradient(135deg,#1e0a3c,#2d1464);padding:2rem 1rem;text-align:center;border-bottom:1px solid var(--border)}
header h1{font-size:2rem;font-weight:800;background:linear-gradient(90deg,#a78bfa,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
header p{color:var(--muted);margin-top:.5rem;font-size:.95rem}
.badge{display:inline-block;background:#7c3aed22;color:#a78bfa;border:1px solid #7c3aed44;border-radius:20px;padding:.2rem .8rem;font-size:.75rem;margin-top:.5rem}
.container{max-width:900px;margin:0 auto;padding:2rem 1rem}
.search-bar{display:flex;gap:.75rem;margin-bottom:2rem;flex-wrap:wrap}
.search-bar input{flex:1;min-width:200px;background:var(--card);border:1px solid var(--border);color:var(--text);padding:.85rem 1.2rem;border-radius:10px;font-size:1rem;outline:none;transition:border .2s}
.search-bar input:focus{border-color:var(--accent2)}
.search-bar button{background:linear-gradient(135deg,#7c3aed,#a855f7);color:#fff;border:none;padding:.85rem 1.8rem;border-radius:10px;font-size:1rem;font-weight:700;cursor:pointer;transition:opacity .2s;white-space:nowrap}
.search-bar button:hover{opacity:.88}
.search-bar button:disabled{opacity:.5;cursor:not-allowed}
.sources{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1.5rem}
.src-btn{background:var(--card);border:1px solid var(--border);color:var(--muted);padding:.4rem 1rem;border-radius:20px;font-size:.8rem;cursor:pointer;transition:all .2s}
.src-btn.active{background:#7c3aed22;border-color:var(--accent);color:#a78bfa}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:1rem}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.25rem;transition:border .2s}
.card:hover{border-color:var(--accent)}
.card-source{font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:.5rem;display:flex;align-items:center;gap:.4rem}
.dot{width:7px;height:7px;border-radius:50%;display:inline-block}
.dot.github{background:#22c55e}.dot.huggingface{background:#f59e0b}.dot.reddit{background:#f97316}
.card h3{font-size:.9rem;font-weight:600;margin-bottom:.4rem;line-height:1.4;word-break:break-word}
.card .meta{font-size:.78rem;color:var(--muted);margin-bottom:.8rem}
.card a{display:inline-block;font-size:.78rem;color:#a78bfa;text-decoration:none;border:1px solid #7c3aed44;padding:.3rem .7rem;border-radius:6px;transition:background .2s}
.card a:hover{background:#7c3aed22}
.section-title{font-size:.85rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin:1.5rem 0 .75rem;border-bottom:1px solid var(--border);padding-bottom:.5rem}
.status{text-align:center;color:var(--muted);padding:3rem 1rem;font-size:.95rem}
.spinner{display:inline-block;width:32px;height:32px;border:3px solid #2a2a3a;border-top-color:#a855f7;border-radius:50%;animation:spin .7s linear infinite;margin-bottom:1rem}
@keyframes spin{to{transform:rotate(360deg)}}
.error-msg{background:#3b0a0a;border:1px solid #7f1d1d;color:#fca5a5;padding:1rem;border-radius:8px;margin-bottom:1rem;font-size:.85rem}
footer{text-align:center;padding:2rem;color:var(--muted);font-size:.75rem;border-top:1px solid var(--border);margin-top:3rem}
@media(max-width:500px){header h1{font-size:1.5rem}}
</style>
</head>
<body>
<header>
<h1>πŸ”­ Trendscout</h1>
<p>Find profitable digital product ideas β€” live from GitHub, HuggingFace & Reddit</p>
<span class="badge">Real-time trend intelligence</span>
</header>
<div class="container">
<div class="search-bar">
<input id="query" type="text" placeholder="Enter your niche (e.g. wellness, AI tools, fitness…)" value="wellness" aria-label="Niche keyword"/>
<button id="scanBtn" onclick="scan()">πŸ” Scan Trends</button>
</div>
<div class="sources">
<span class="src-btn active" data-src="github">⚑ GitHub</span>
<span class="src-btn active" data-src="huggingface">πŸ€— HuggingFace</span>
<span class="src-btn active" data-src="reddit">πŸ’¬ Reddit</span>
</div>
<div id="results"><p class="status">Enter a niche above and hit <strong>Scan Trends</strong> to find what's hot right now.</p></div>
</div>
<footer>Trendscout β€” Built for digital creators, coaches &amp; entrepreneurs. Stop guessing. Start building. πŸš€</footer>
<script>
const activeSources=new Set(['github','huggingface','reddit']);
document.querySelectorAll('.src-btn').forEach(b=>{
b.addEventListener('click',()=>{
const s=b.dataset.src;
if(activeSources.has(s)){if(activeSources.size>1){activeSources.delete(s);b.classList.remove('active');}}
else{activeSources.add(s);b.classList.add('active');}
});
});
async function scan(){
const q=document.getElementById('query').value.trim()||'wellness';
const btn=document.getElementById('scanBtn');
btn.disabled=true; btn.textContent='Scanning…';
document.getElementById('results').innerHTML='<div class="status"><div class="spinner"></div><br/>Scanning live sources for <strong>'+q+'</strong>…</div>';
const fetches=[];
if(activeSources.has('github')) fetches.push(fetch('/api/github?topic='+encodeURIComponent(q)).then(r=>r.json()).then(d=>({src:'github',items:d})).catch(()=>({src:'github',items:[]})));
if(activeSources.has('huggingface')) fetches.push(fetch('/api/huggingface?topic='+encodeURIComponent(q)).then(r=>r.json()).then(d=>({src:'huggingface',items:d})).catch(()=>({src:'huggingface',items:[]})));
if(activeSources.has('reddit')) fetches.push(fetch('/api/reddit?topic='+encodeURIComponent(q)).then(r=>r.json()).then(d=>({src:'reddit',items:d})).catch(()=>({src:'reddit',items:[]})));
const results=await Promise.all(fetches);
let html='';
const colors={github:'#22c55e',huggingface:'#f59e0b',reddit:'#f97316'};
const labels={github:'GitHub Trending',huggingface:'HuggingFace Models',reddit:'Reddit Buzz'};
let totalItems=0;
results.forEach(({src,items})=>{
if(!items||!items.length) return;
totalItems+=items.length;
html+='<div class="section-title"><span class="dot '+src+'" style="background:'+colors[src]+'"></span> '+labels[src]+'</div><div class="grid">';
items.forEach(item=>{
html+='<div class="card"><div class="card-source"><span class="dot '+src+'" style="background:'+colors[src]+'"></span>'+src.toUpperCase()+'</div>';
html+='<h3>'+escHtml(item.title)+'</h3><div class="meta">'+escHtml(item.meta||'')+'</div>';
html+='<a href="'+escHtml(item.url)+'" target="_blank" rel="noopener">View Source β†’</a></div>';
});
html+='</div>';
});
if(!totalItems) html='<p class="status">No results found for <strong>'+escHtml(q)+'</strong>. Try a broader keyword.</p>';
document.getElementById('results').innerHTML=html;
btn.disabled=false; btn.textContent='πŸ” Scan Trends';
}
function escHtml(s){
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Auto-scan on load
window.addEventListener('DOMContentLoaded',()=>scan());
</script>
</body>
</html>`);
});
// ── API Proxies ─────────────────────────────────────────────────────
app.get('/api/github', async (req, res) => {
const topic = req.query.topic || 'wellness';
try {
const since = new Date(Date.now() - 30*24*60*60*1000).toISOString().split('T')[0];
const q = `${topic} created:>${since}`;
const response = await axios.get('https://api.github.com/search/repositories', {
params: { q, sort: 'stars', order: 'desc', per_page: 6 },
headers: { 'User-Agent': 'Trendscout/1.0', ...(process.env.GITHUB_TOKEN ? { Authorization: `token ${process.env.GITHUB_TOKEN}` } : {}) }
});
const items = (response.data.items || []).map(r => ({
title: r.full_name,
url: r.html_url,
meta: `β˜… ${r.stargazers_count.toLocaleString()} stars Β· ${r.language || 'multi'} Β· ${r.description ? r.description.slice(0,60)+'…' : ''}`
}));
res.json(items);
} catch (err) {
res.status(500).json([]);
}
});
app.get('/api/huggingface', async (req, res) => {
const topic = req.query.topic || 'wellness';
try {
const response = await axios.get('https://huggingface.co/api/models', {
params: { search: topic, sort: 'likes', direction: '-1', limit: 6 }
});
const items = (response.data || []).map(m => ({
title: m.id,
url: `https://huggingface.co/${m.id}`,
meta: `β™₯ ${m.likes ?? 0} likes Β· ${(m.tags||[]).slice(0,3).join(', ')}`
}));
res.json(items);
} catch (err) {
res.status(500).json([]);
}
});
app.get('/api/reddit', async (req, res) => {
const topic = req.query.topic || 'wellness';
try {
const response = await axios.get(`https://www.reddit.com/r/Entrepreneur/search.json`, {
params: { q: topic, restrict_sr: 1, sort: 'top', t: 'month', limit: 6 },
headers: { 'User-Agent': 'Trendscout/1.0' }
});
const items = (response.data.data?.children || []).map(p => ({
title: p.data.title,
url: 'https://reddit.com' + p.data.permalink,
meta: `β–² ${p.data.ups.toLocaleString()} upvotes Β· ${p.data.num_comments} comments`
}));
res.json(items);
} catch (err) {
res.status(500).json([]);
}
});
const PORT = process.env.PORT || 7860;
app.listen(PORT, () => console.log(`βœ… Trendscout running on port ${PORT}`));