vipcare 0.3.12 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/vip.js +31 -3
- package/lib/card.js +53 -0
- package/package.json +1 -1
package/bin/vip.js
CHANGED
|
@@ -474,16 +474,44 @@ program.command('youtube-search')
|
|
|
474
474
|
|
|
475
475
|
// --- card ---
|
|
476
476
|
program.command('card')
|
|
477
|
-
.description('Generate H5 baseball card page
|
|
477
|
+
.description('Generate and serve H5 baseball card page')
|
|
478
478
|
.option('-o, --output <path>', 'Output HTML file', 'web/index.html')
|
|
479
|
-
.
|
|
479
|
+
.option('-p, --port <port>', 'Server port', '3000')
|
|
480
|
+
.option('--no-serve', 'Only generate, do not start server')
|
|
481
|
+
.action(async (opts) => {
|
|
480
482
|
console.log(c.cyan('Generating baseball cards...'));
|
|
481
483
|
const profiles = listProfiles();
|
|
482
484
|
if (!profiles.length) { console.log(c.dim('No profiles. Use "vip add" first.')); return; }
|
|
483
485
|
|
|
484
486
|
const outputPath = generateCards(profiles, opts.output);
|
|
485
487
|
console.log(c.green(`Cards generated: ${outputPath}`));
|
|
486
|
-
|
|
488
|
+
|
|
489
|
+
if (opts.serve === false) {
|
|
490
|
+
console.log(c.dim(`Open in browser: open ${outputPath}`));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const http = await import('http');
|
|
495
|
+
const port = parseInt(opts.port);
|
|
496
|
+
const dir = path.dirname(outputPath);
|
|
497
|
+
const file = path.basename(outputPath);
|
|
498
|
+
|
|
499
|
+
const server = http.createServer((req, res) => {
|
|
500
|
+
const filePath = req.url === '/' ? path.join(dir, file) : path.join(dir, req.url);
|
|
501
|
+
if (!fs.existsSync(filePath)) { res.writeHead(404); res.end('Not found'); return; }
|
|
502
|
+
const ext = path.extname(filePath);
|
|
503
|
+
const types = { '.html': 'text/html', '.css': 'text/css', '.js': 'text/javascript', '.json': 'application/json', '.png': 'image/png' };
|
|
504
|
+
res.writeHead(200, { 'Content-Type': types[ext] || 'text/plain' });
|
|
505
|
+
res.end(fs.readFileSync(filePath));
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
server.listen(port, () => {
|
|
509
|
+
const url = `http://localhost:${port}`;
|
|
510
|
+
console.log(c.green(`\nServer running at ${c.bold(url)}`));
|
|
511
|
+
console.log(c.dim('Press Ctrl+C to stop\n'));
|
|
512
|
+
// Auto-open in browser
|
|
513
|
+
try { execFileSync('open', [url], { stdio: 'ignore' }); } catch {}
|
|
514
|
+
});
|
|
487
515
|
});
|
|
488
516
|
|
|
489
517
|
// --- digest ---
|
package/lib/card.js
CHANGED
|
@@ -31,6 +31,7 @@ export function generateCards(profiles, outputPath = 'web/index.html') {
|
|
|
31
31
|
const industryMatch = content.match(/\*\*Industry:\*\*\s*(.+)/);
|
|
32
32
|
|
|
33
33
|
cards.push({
|
|
34
|
+
slug: p.slug,
|
|
34
35
|
name: nameMatch ? nameMatch[1] : p.name,
|
|
35
36
|
title: titleMatch ? titleMatch[1].trim() : '',
|
|
36
37
|
company: companyMatch ? companyMatch[1].trim() : '',
|
|
@@ -50,9 +51,20 @@ export function generateCards(profiles, outputPath = 'web/index.html') {
|
|
|
50
51
|
continue;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
data.slug = p.slug;
|
|
53
55
|
cards.push(data);
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
// Generate individual profile pages
|
|
59
|
+
for (const p of profiles) {
|
|
60
|
+
const content = loadProfile(p.slug);
|
|
61
|
+
if (!content) continue;
|
|
62
|
+
const profileHtml = buildProfilePage(p.name, content);
|
|
63
|
+
const dir = path.dirname(outputPath);
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
fs.writeFileSync(path.join(dir, `${p.slug}.html`), profileHtml, 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
const html = buildHtml(cards);
|
|
57
69
|
|
|
58
70
|
const dir = path.dirname(outputPath);
|
|
@@ -62,6 +74,45 @@ export function generateCards(profiles, outputPath = 'web/index.html') {
|
|
|
62
74
|
return path.resolve(outputPath);
|
|
63
75
|
}
|
|
64
76
|
|
|
77
|
+
function buildProfilePage(name, markdown) {
|
|
78
|
+
// Convert markdown to simple HTML
|
|
79
|
+
const htmlContent = markdown
|
|
80
|
+
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
81
|
+
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
82
|
+
.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
|
|
83
|
+
.replace(/^- \*\*(.+?):\*\* (.+)$/gm, '<p><strong>$1:</strong> $2</p>')
|
|
84
|
+
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
85
|
+
.replace(/\n\n/g, '<br><br>');
|
|
86
|
+
|
|
87
|
+
return `<!DOCTYPE html>
|
|
88
|
+
<html lang="en">
|
|
89
|
+
<head>
|
|
90
|
+
<meta charset="UTF-8">
|
|
91
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
92
|
+
<title>${escapeHtml(name)} - VIPCare</title>
|
|
93
|
+
<style>
|
|
94
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
95
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; max-width: 800px; margin: 0 auto; line-height: 1.7; }
|
|
96
|
+
a { color: #38bdf8; text-decoration: none; }
|
|
97
|
+
a:hover { text-decoration: underline; }
|
|
98
|
+
.back { display: inline-block; margin-bottom: 20px; font-size: 0.9em; }
|
|
99
|
+
h1 { color: #38bdf8; font-size: 2em; margin: 16px 0 8px; }
|
|
100
|
+
h2 { color: #818cf8; font-size: 1.3em; margin: 24px 0 8px; border-bottom: 1px solid #334155; padding-bottom: 6px; }
|
|
101
|
+
blockquote { color: #fbbf24; font-style: italic; padding: 8px 16px; border-left: 3px solid #475569; margin: 8px 0; }
|
|
102
|
+
p, li { color: #cbd5e1; margin: 4px 0; }
|
|
103
|
+
strong { color: #f1f5f9; }
|
|
104
|
+
li { margin-left: 20px; }
|
|
105
|
+
hr { border: none; border-top: 1px solid #334155; margin: 24px 0; }
|
|
106
|
+
.meta { color: #64748b; font-size: 0.85em; }
|
|
107
|
+
</style>
|
|
108
|
+
</head>
|
|
109
|
+
<body>
|
|
110
|
+
<a href="index.html" class="back">← Back to all cards</a>
|
|
111
|
+
${htmlContent}
|
|
112
|
+
</body>
|
|
113
|
+
</html>`;
|
|
114
|
+
}
|
|
115
|
+
|
|
65
116
|
function escapeHtml(str) {
|
|
66
117
|
return String(str)
|
|
67
118
|
.replace(/&/g, '&')
|
|
@@ -286,6 +337,8 @@ function openModal(index) {
|
|
|
286
337
|
\${card.gifts?.length ? \`<p><strong>🎁 Gifts:</strong> \${card.gifts.join(', ')}</p>\` : ''}
|
|
287
338
|
|
|
288
339
|
\${card.tags?.length ? \`<h2>Tags</h2><div class="tags">\${card.tags.map(t => \`<span class="tag">\${t}</span>\`).join('')}</div>\` : ''}
|
|
340
|
+
|
|
341
|
+
\${card.slug ? \`<p style="margin-top:20px;text-align:center"><a href="\${card.slug}.html" style="color:#38bdf8;text-decoration:none;padding:8px 20px;border:1px solid #38bdf8;border-radius:8px;display:inline-block">View Full Profile →</a></p>\` : ''}
|
|
289
342
|
\`;
|
|
290
343
|
|
|
291
344
|
document.getElementById('modal').classList.add('active');
|