skander101 commited on
Commit
c97c10a
·
1 Parent(s): ee82dfb

Add sponsors page + /api/v1/sponsors endpoint

Browse files
Files changed (3) hide show
  1. templates/index.html +6 -3
  2. templates/sponsors.html +126 -0
  3. webapp.py +31 -0
templates/index.html CHANGED
@@ -194,9 +194,12 @@ body {
194
  <h1>News Digest</h1>
195
  <span>{{ total }} stories across {{ categories|length }} topics</span>
196
  </div>
197
- <div class="status">
198
- <span class="status-dot"></span>
199
- {{ status }}
 
 
 
200
  </div>
201
  </div>
202
  </div>
 
194
  <h1>News Digest</h1>
195
  <span>{{ total }} stories across {{ categories|length }} topics</span>
196
  </div>
197
+ <div style="display:flex; align-items:center; gap:12px;">
198
+ <a href="/sponsors" class="btn" style="background:#334155; color:#94a3b8; font-size:13px;">🏢 Sponsors</a>
199
+ <span class="status">
200
+ <span class="status-dot"></span>
201
+ {{ status }}
202
+ </span>
203
  </div>
204
  </div>
205
  </div>
templates/sponsors.html ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sponsors — News Digest</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:opsz@14..32&display=swap" rel="stylesheet">
8
+ <style>
9
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
+ body {
11
+ font-family: 'Inter', -apple-system, sans-serif;
12
+ background: #0f172a;
13
+ color: #e2e8f0;
14
+ min-height: 100vh;
15
+ }
16
+ .header {
17
+ background: linear-gradient(135deg, #1e293b, #0f172a);
18
+ border-bottom: 1px solid #1e293b;
19
+ padding: 20px 32px;
20
+ position: sticky;
21
+ top: 0;
22
+ z-index: 50;
23
+ backdrop-filter: blur(12px);
24
+ }
25
+ .header-inner {
26
+ max-width: 1400px;
27
+ margin: 0 auto;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: space-between;
31
+ flex-wrap: wrap;
32
+ gap: 12px;
33
+ }
34
+ .header h1 {
35
+ font-size: 22px;
36
+ font-weight: 700;
37
+ background: linear-gradient(135deg, #60a5fa, #a78bfa);
38
+ -webkit-background-clip: text;
39
+ -webkit-text-fill-color: transparent;
40
+ }
41
+ .back-btn {
42
+ padding: 8px 18px;
43
+ border-radius: 8px;
44
+ font-size: 13px;
45
+ font-weight: 500;
46
+ text-decoration: none;
47
+ background: #334155;
48
+ color: #94a3b8;
49
+ transition: background .15s;
50
+ }
51
+ .back-btn:hover { background: #475569; color: #e2e8f0; }
52
+ .container { max-width: 1000px; margin: 0 auto; padding: 32px; }
53
+ .grid { display: grid; gap: 16px; }
54
+ .card {
55
+ background: #1e293b;
56
+ border-radius: 14px;
57
+ padding: 20px 24px;
58
+ border: 1px solid #334155;
59
+ }
60
+ .card h2 {
61
+ font-size: 16px;
62
+ font-weight: 600;
63
+ color: #fbbf24;
64
+ margin-bottom: 4px;
65
+ }
66
+ .card .sub {
67
+ font-size: 12px;
68
+ color: #64748b;
69
+ margin-bottom: 12px;
70
+ }
71
+ .sources { display: flex; flex-wrap: wrap; gap: 6px; }
72
+ .source-tag {
73
+ font-size: 12px;
74
+ padding: 4px 10px;
75
+ border-radius: 6px;
76
+ background: #334155;
77
+ color: #94a3b8;
78
+ border: 1px solid #475569;
79
+ }
80
+ .empty { text-align: center; padding: 60px 20px; color: #64748b; }
81
+ .empty h2 { font-size: 18px; margin-bottom: 6px; }
82
+ .count-badge {
83
+ font-size: 11px;
84
+ color: #818cf8;
85
+ background: rgba(129, 140, 248, 0.12);
86
+ padding: 2px 8px;
87
+ border-radius: 5px;
88
+ margin-left: 8px;
89
+ }
90
+ @media (max-width: 640px) {
91
+ .header { padding: 14px; }
92
+ .container { padding: 14px; }
93
+ }
94
+ </style>
95
+ </head>
96
+ <body>
97
+ <div class="header">
98
+ <div class="header-inner">
99
+ <h1>News Digest — Sponsors</h1>
100
+ <a href="/" class="back-btn">← Back to News</a>
101
+ </div>
102
+ </div>
103
+ <div class="container">
104
+ {% if sponsors %}
105
+ <div class="grid">
106
+ {% for s in sponsors %}
107
+ <div class="card">
108
+ <h2>🏢 {{ s.name }}</h2>
109
+ <div class="sub">{{ s.source_count }} source{% if s.source_count != 1 %}s{% endif %}</div>
110
+ <div class="sources">
111
+ {% for domain in s.sources %}
112
+ <span class="source-tag">{{ domain }}</span>
113
+ {% endfor %}
114
+ </div>
115
+ </div>
116
+ {% endfor %}
117
+ </div>
118
+ {% else %}
119
+ <div class="empty">
120
+ <h2>No sponsor data yet</h2>
121
+ <p>Run a data refresh to populate sponsor information</p>
122
+ </div>
123
+ {% endif %}
124
+ </div>
125
+ </body>
126
+ </html>
webapp.py CHANGED
@@ -224,6 +224,37 @@ def api_categories():
224
  return jsonify({"categories": CATEGORIES})
225
 
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  @app.route("/api/v1/refresh", methods=["POST"])
228
  def api_refresh():
229
  threading.Thread(target=refresh_data, daemon=True).start()
 
224
  return jsonify({"categories": CATEGORIES})
225
 
226
 
227
+ def _build_sponsors():
228
+ with cache_lock:
229
+ results = list(cached_results)
230
+ groups: dict[str, set[str]] = {}
231
+ for cluster in results:
232
+ for article in cluster.get("articles", []):
233
+ sponsor = article.get("sponsor", "")
234
+ domain = article.get("domain", "")
235
+ if sponsor:
236
+ if sponsor not in groups:
237
+ groups[sponsor] = set()
238
+ groups[sponsor].add(domain)
239
+ return sorted(
240
+ [
241
+ {"name": s, "sources": sorted(doms), "source_count": len(doms)}
242
+ for s, doms in groups.items()
243
+ ],
244
+ key=lambda x: x["name"].lower(),
245
+ )
246
+
247
+
248
+ @app.route("/api/v1/sponsors")
249
+ def api_sponsors():
250
+ return jsonify({"sponsors": _build_sponsors()})
251
+
252
+
253
+ @app.route("/sponsors")
254
+ def sponsors_page():
255
+ return render_template("sponsors.html", sponsors=_build_sponsors(), categories=CATEGORIES)
256
+
257
+
258
  @app.route("/api/v1/refresh", methods=["POST"])
259
  def api_refresh():
260
  threading.Thread(target=refresh_data, daemon=True).start()