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.
Files changed (3) hide show
  1. package/bin/vip.js +31 -3
  2. package/lib/card.js +53 -0
  3. 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 from all profiles')
477
+ .description('Generate and serve H5 baseball card page')
478
478
  .option('-o, --output <path>', 'Output HTML file', 'web/index.html')
479
- .action((opts) => {
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
- console.log(c.dim(`Open in browser: open ${outputPath}`));
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">&larr; 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, '&amp;')
@@ -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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vipcare",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "Auto-build VIP person profiles from Twitter/LinkedIn public data",
5
5
  "type": "module",
6
6
  "bin": {