Skip to content

Commit 5a46bb1

Browse files
polunzhclaude
andcommitted
feat: serve demo page at /demo.html on production domain
- Add demo.html to web/public (auto-copied to dist by Vite) - Update README demo links to devpulse.127.dev/demo.html - Requires Cloudflare Access bypass rule for /demo.html path Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 35e5a2e commit 5a46bb1

3 files changed

Lines changed: 263 additions & 4 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
[![CI](https://github.com/polunzh/devpulse/actions/workflows/ci.yml/badge.svg)](https://github.com/polunzh/devpulse/actions/workflows/ci.yml)
44
![Coverage](https://github.com/polunzh/devpulse/blob/master/.github/badges/coverage.svg?raw=true)
5-
[![Live Demo](https://img.shields.io/badge/demo-live-f97316)](https://polunzh.github.io/devpulse/)
5+
[![Live Demo](https://img.shields.io/badge/demo-live-f97316)](https://devpulse.127.dev/demo.html)
66

7-
[中文文档](./README.zh-CN.md) | [Live Demo](https://polunzh.github.io/devpulse/)
7+
[中文文档](./README.zh-CN.md) | [Live Demo](https://devpulse.127.dev/demo.html)
88

99
A personal developer hotspot content aggregator. Collects trending posts from HackerNews, Reddit, V2EX, Medium and more, with AI-powered personalized recommendations.
1010

README.zh-CN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
[![CI](https://github.com/polunzh/devpulse/actions/workflows/ci.yml/badge.svg)](https://github.com/polunzh/devpulse/actions/workflows/ci.yml)
44
![Coverage](https://github.com/polunzh/devpulse/blob/master/.github/badges/coverage.svg?raw=true)
5-
[![Live Demo](https://img.shields.io/badge/demo-live-f97316)](https://polunzh.github.io/devpulse/)
5+
[![Live Demo](https://img.shields.io/badge/demo-live-f97316)](https://devpulse.127.dev/demo.html)
66

7-
[English](./README.md) | [在线演示](https://polunzh.github.io/devpulse/)
7+
[English](./README.md) | [在线演示](https://devpulse.127.dev/demo.html)
88

99
个人开发者热点内容聚合工具。从 HackerNews、Reddit、V2EX、Medium 等站点收集热门内容,支持 AI 个性化推荐。
1010

packages/web/public/demo.html

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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>DevPulse Demo</title>
7+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
8+
<style>
9+
:root {
10+
--color-primary: #f97316;
11+
--color-primary-light: #fff7ed;
12+
--color-primary-dark: #c2410c;
13+
--color-primary-hover: #ea580c;
14+
--color-secondary: #6366f1;
15+
--color-secondary-light: #eef2ff;
16+
--color-accent: #10b981;
17+
--color-accent-light: #ecfdf5;
18+
--color-error: #ef4444;
19+
--color-error-bg: #fef2f2;
20+
--color-bg: #fffbf7;
21+
--color-surface: #ffffff;
22+
--color-border: #f0e6db;
23+
--color-border-light: #f7f0e8;
24+
--color-text: #292017;
25+
--color-text-secondary: #78716c;
26+
--color-text-muted: #a8a29e;
27+
--shadow-md: 0 2px 8px rgba(41, 32, 23, 0.08);
28+
}
29+
30+
*, *::before, *::after { box-sizing: border-box; margin: 0; }
31+
32+
body {
33+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans SC', sans-serif;
34+
font-size: 14px; line-height: 1.5;
35+
color: var(--color-text); background: var(--color-bg);
36+
-webkit-font-smoothing: antialiased;
37+
}
38+
39+
a { color: var(--color-primary); text-decoration: none; }
40+
41+
/* Demo banner */
42+
.demo-banner {
43+
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
44+
color: white; text-align: center; padding: 12px 16px; font-size: 13px;
45+
}
46+
.demo-banner a { color: white; text-decoration: underline; font-weight: 600; }
47+
48+
/* Feed */
49+
.feed { max-width: 800px; margin: 0 auto; background: var(--color-surface); min-height: 100vh; box-shadow: var(--shadow-md); }
50+
.feed-header { display: flex; align-items: center; justify-content: space-between; padding: 16px; border-bottom: 1px solid var(--color-border); }
51+
.feed-header h1 { font-size: 20px; }
52+
.logo-icon { color: var(--color-primary); }
53+
.feed-actions { display: flex; align-items: center; gap: 12px; font-size: 13px; }
54+
.feed-actions button, .feed-actions select {
55+
padding: 6px 14px; cursor: pointer; border: 1px solid var(--color-border);
56+
border-radius: 6px; background: var(--color-surface); color: var(--color-text);
57+
font-family: inherit; font-size: inherit; transition: all 0.15s;
58+
}
59+
.feed-actions button:hover { background: var(--color-primary-light); border-color: var(--color-primary); color: var(--color-primary); }
60+
.feed-actions a { color: var(--color-secondary); font-weight: 500; }
61+
62+
/* Site filter */
63+
.site-filter { display: flex; gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--color-border-light); flex-wrap: wrap; }
64+
.site-filter button {
65+
padding: 5px 14px; border: 1px solid var(--color-border); border-radius: 16px;
66+
background: var(--color-surface); cursor: pointer; font-size: 13px;
67+
color: var(--color-text-secondary); font-family: inherit; transition: all 0.15s;
68+
}
69+
.site-filter button:hover:not(.active) { background: var(--color-primary-light); border-color: var(--color-primary); color: var(--color-primary); }
70+
.site-filter button.active { background: var(--color-primary); color: white; border-color: var(--color-primary); font-weight: 500; }
71+
72+
/* Post item */
73+
.post-item {
74+
padding: 14px 16px; border-bottom: 1px solid var(--color-border-light);
75+
cursor: pointer; transition: all 0.15s;
76+
}
77+
.post-item:hover { background: var(--color-primary-light); }
78+
.post-item.read { opacity: 0.6; background: var(--color-bg); }
79+
.post-item.read .post-title { color: var(--color-text-secondary); }
80+
.post-title { margin: 0 0 5px; font-size: 15px; font-weight: 500; line-height: 1.4; }
81+
.post-meta { font-size: 12px; color: var(--color-text-secondary); display: flex; gap: 12px; align-items: center; }
82+
.post-source {
83+
color: var(--color-secondary); font-weight: 600;
84+
background: var(--color-secondary-light); padding: 1px 8px;
85+
border-radius: 10px; font-size: 11px;
86+
}
87+
.post-score { color: var(--color-primary); font-weight: 600; }
88+
.post-author, .post-time { color: var(--color-text-muted); }
89+
.post-reason {
90+
font-size: 13px; color: var(--color-text-secondary);
91+
margin: 6px 0 0; padding-left: 8px; border-left: 2px solid var(--color-accent);
92+
}
93+
94+
/* Hiding animation */
95+
.post-hiding { animation: slideOut 0.4s ease-out forwards; }
96+
@keyframes slideOut {
97+
0% { opacity: 0.5; transform: translateX(0); }
98+
50% { opacity: 0.2; transform: translateX(30px); }
99+
100% { opacity: 0; height: 0; padding: 0; margin: 0; border: none; transform: translateX(60px); overflow: hidden; }
100+
}
101+
102+
/* Mobile */
103+
@media (max-width: 640px) {
104+
.feed-header { flex-wrap: wrap; gap: 8px; }
105+
.feed-actions { flex-wrap: wrap; }
106+
.demo-banner { font-size: 12px; }
107+
}
108+
</style>
109+
</head>
110+
<body>
111+
<div class="demo-banner">
112+
This is a live demo with sample data.
113+
<a href="https://github.com/polunzh/devpulse">View on GitHub</a> |
114+
<a href="https://github.com/polunzh/devpulse#quick-start">Deploy your own</a>
115+
</div>
116+
117+
<div class="feed" id="app">
118+
<header class="feed-header">
119+
<h1><span class="logo-icon"></span> DevPulse</h1>
120+
<div class="feed-actions">
121+
<label><input type="checkbox" id="hideRead"> Hide read</label>
122+
<select id="sortBy">
123+
<option value="score">By relevance</option>
124+
<option value="time">By time</option>
125+
</select>
126+
<button onclick="refreshPosts()">Refresh</button>
127+
<a href="#settings" onclick="alert('Settings page available in full deployment')">Settings</a>
128+
</div>
129+
</header>
130+
131+
<div class="site-filter" id="siteFilter"></div>
132+
<div id="postList"></div>
133+
</div>
134+
135+
<script>
136+
const MOCK_SITES = [
137+
{ id: 's1', name: 'Hacker News' },
138+
{ id: 's2', name: 'Reddit/programming' },
139+
{ id: 's3', name: 'Reddit/rust' },
140+
{ id: 's4', name: 'V2EX' },
141+
];
142+
143+
const MOCK_POSTS = [
144+
{ id: '1', siteId: 's1', title: 'Show HN: I built a real-time collaborative code editor in Rust', url: '#', author: 'rustdev', score: 842, aiScore: 0.95, aiReason: 'Rust 项目,与你的兴趣高度匹配', publishedAt: new Date(Date.now() - 2*3600000).toISOString() },
145+
{ id: '2', siteId: 's1', title: 'The State of AI in 2026: What Changed', url: '#', author: 'mlresearcher', score: 651, aiScore: 0.92, aiReason: 'AI 领域深度分析,推荐阅读', publishedAt: new Date(Date.now() - 3*3600000).toISOString() },
146+
{ id: '3', siteId: 's2', title: 'Why we migrated from microservices back to a monolith', url: '#', author: 'sre_engineer', score: 523, aiScore: 0.88, aiReason: '系统设计实践经验,架构决策参考', publishedAt: new Date(Date.now() - 4*3600000).toISOString() },
147+
{ id: '4', siteId: 's1', title: 'PostgreSQL 18 Released with Major Performance Improvements', url: '#', author: 'pgfan', score: 489, aiScore: 0.75, aiReason: '数据库重大更新,值得关注', publishedAt: new Date(Date.now() - 5*3600000).toISOString() },
148+
{ id: '5', siteId: 's3', title: 'Announcing Tokio 2.0: The Async Runtime Gets a Major Overhaul', url: '#', author: 'tokio_team', score: 378, aiScore: 0.93, aiReason: 'Rust 异步运行时核心更新', publishedAt: new Date(Date.now() - 1*3600000).toISOString() },
149+
{ id: '6', siteId: 's2', title: 'How Discord stores trillions of messages', url: '#', author: 'discord_eng', score: 412, aiScore: 0.85, aiReason: '大规模系统设计案例', publishedAt: new Date(Date.now() - 6*3600000).toISOString() },
150+
{ id: '7', siteId: 's4', title: '分享一个我做的开源项目,用了半年时间', url: '#', author: 'v2exer', score: 156, aiScore: 0.65, aiReason: '开源项目分享,社区讨论', publishedAt: new Date(Date.now() - 7*3600000).toISOString() },
151+
{ id: '8', siteId: 's1', title: 'CSS just got container queries and they change everything', url: '#', author: 'csswizard', score: 298, aiScore: 0.45, aiReason: '前端技术更新', publishedAt: new Date(Date.now() - 8*3600000).toISOString() },
152+
{ id: '9', siteId: 's3', title: 'Writing a compiler in Rust: A practical guide', url: '#', author: 'compiler_nerd', score: 267, aiScore: 0.91, aiReason: 'Rust 编译器实践,技术深度高', publishedAt: new Date(Date.now() - 2.5*3600000).toISOString() },
153+
{ id: '10', siteId: 's4', title: '最近 AI 编程工具测评,Claude Code vs Cursor', url: '#', author: 'techblogger', score: 203, aiScore: 0.78, aiReason: 'AI 工具对比,实用参考', publishedAt: new Date(Date.now() - 9*3600000).toISOString() },
154+
{ id: '11', siteId: 's2', title: 'TIL: You can use SQLite as a message queue', url: '#', author: 'pragmatic_dev', score: 341, aiScore: 0.72, aiReason: 'SQLite 创新用法', publishedAt: new Date(Date.now() - 10*3600000).toISOString() },
155+
{ id: '12', siteId: 's1', title: 'The smallest Go binary ever: 14 bytes', url: '#', author: 'gopher', score: 189, aiScore: 0.55, aiReason: '有趣的技术挑战', publishedAt: new Date(Date.now() - 12*3600000).toISOString() },
156+
];
157+
158+
let activeSiteId = null;
159+
let readIds = new Set();
160+
let currentSort = 'score';
161+
162+
function timeAgo(dateStr) {
163+
const diff = Date.now() - new Date(dateStr).getTime();
164+
const hours = Math.floor(diff / 3600000);
165+
if (hours < 1) return 'just now';
166+
if (hours < 24) return hours + 'h ago';
167+
return Math.floor(hours / 24) + 'd ago';
168+
}
169+
170+
function renderSiteFilter() {
171+
const el = document.getElementById('siteFilter');
172+
let html = `<button class="${!activeSiteId ? 'active' : ''}" onclick="filterSite(null)">All</button>`;
173+
MOCK_SITES.forEach(s => {
174+
html += `<button class="${activeSiteId === s.id ? 'active' : ''}" onclick="filterSite('${s.id}')">${s.name}</button>`;
175+
});
176+
el.innerHTML = html;
177+
}
178+
179+
function getSortedPosts() {
180+
let posts = [...MOCK_POSTS];
181+
if (activeSiteId) posts = posts.filter(p => p.siteId === activeSiteId);
182+
183+
const hideRead = document.getElementById('hideRead').checked;
184+
if (hideRead) posts = posts.filter(p => !readIds.has(p.id));
185+
186+
if (currentSort === 'score') {
187+
posts.sort((a, b) => {
188+
const aFinal = (a.score / 842) * 0.4 + a.aiScore * 0.6;
189+
const bFinal = (b.score / 842) * 0.4 + b.aiScore * 0.6;
190+
return bFinal - aFinal;
191+
});
192+
} else {
193+
posts.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
194+
}
195+
return posts;
196+
}
197+
198+
function renderPosts() {
199+
const posts = getSortedPosts();
200+
const el = document.getElementById('postList');
201+
if (posts.length === 0) {
202+
el.innerHTML = '<p style="text-align:center;color:var(--color-text-muted);padding:60px 20px;">No posts to show.</p>';
203+
return;
204+
}
205+
const siteName = (siteId) => MOCK_SITES.find(s => s.id === siteId)?.name || '';
206+
el.innerHTML = posts.map(p => `
207+
<div class="post-item ${readIds.has(p.id) ? 'read' : ''}" id="post-${p.id}" onclick="markRead('${p.id}')">
208+
<h3 class="post-title">${p.title}</h3>
209+
<div class="post-meta">
210+
<span class="post-source">${siteName(p.siteId)}</span>
211+
<span class="post-score">▲ ${p.score}</span>
212+
<span class="post-author">by ${p.author}</span>
213+
<span class="post-time">${timeAgo(p.publishedAt)}</span>
214+
</div>
215+
${p.aiReason ? `<p class="post-reason">${p.aiReason}</p>` : ''}
216+
</div>
217+
`).join('');
218+
}
219+
220+
function filterSite(id) {
221+
activeSiteId = id;
222+
renderSiteFilter();
223+
renderPosts();
224+
}
225+
226+
function markRead(id) {
227+
readIds.add(id);
228+
const el = document.getElementById('post-' + id);
229+
if (el) el.classList.add('read');
230+
231+
if (document.getElementById('hideRead').checked) {
232+
setTimeout(() => {
233+
if (el) el.classList.add('post-hiding');
234+
setTimeout(() => renderPosts(), 400);
235+
}, 800);
236+
}
237+
}
238+
239+
function refreshPosts() {
240+
const btn = event.target;
241+
btn.textContent = 'Fetching...';
242+
btn.disabled = true;
243+
setTimeout(() => {
244+
btn.textContent = 'Refresh';
245+
btn.disabled = false;
246+
}, 1500);
247+
}
248+
249+
document.getElementById('sortBy').addEventListener('change', (e) => {
250+
currentSort = e.target.value;
251+
renderPosts();
252+
});
253+
document.getElementById('hideRead').addEventListener('change', () => renderPosts());
254+
255+
renderSiteFilter();
256+
renderPosts();
257+
</script>
258+
</body>
259+
</html>

0 commit comments

Comments
 (0)