Skip to content

Commit cfe2d3e

Browse files
committed
feature: interactive mode like qrcode , clipboard etc
1 parent caf112a commit cfe2d3e

File tree

4 files changed

+1510
-49
lines changed

4 files changed

+1510
-49
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,17 @@ npx bharathkumar-palanisamy
1616
# 3) Scoped fallback with slash (alternative)
1717
npx @bharathkumar-palanisamy/resume
1818
```
19-
19+
## Launch interactive mode
20+
```bash
21+
npx bharathkumar-palanisamy --interactive
22+
23+
## Traditional CLI usage still works
24+
npx bharathkumar-palanisamy --format json --section personal
25+
npx bharathkumar-palanisamy --help
26+
npx bharathkumar-palanisamy --version
27+
npx bharathkumar-palanisamy --section personal experience
28+
npx bharathkumar-palanisamy --format plain --output my-resume.txt
29+
```
2030
## Local testing
2131

2232
```bash

cli.js

Lines changed: 305 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { Command } from "commander";
55
import fs from "fs";
66
import path from "path";
77
import { fileURLToPath } from "url";
8+
import inquirer from "inquirer";
9+
import qrcode from "qrcode";
10+
import clipboardy from "clipboardy";
811

912
const __filename = fileURLToPath(import.meta.url);
1013
const __dirname = path.dirname(__filename);
@@ -20,7 +23,7 @@ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.jso
2023
*/
2124

2225
// Resume data structure
23-
const resumeData = {
26+
export const resumeData = {
2427
personal: {
2528
name: "Bharathkumar Palanisamy",
2629
role: "Full-Stack Engineer (JavaScript / Node.js & React)",
@@ -298,7 +301,14 @@ program
298301
.option('-f, --format <type>', 'output format (colored, plain, json)', 'colored')
299302
.option('-s, --section <sections...>', 'specific sections to display (personal, profile, techStack, experience, projects, leadership, openSource, education)')
300303
.option('-o, --output <file>', 'save resume to file')
301-
.action((options) => {
304+
.option('-i, --interactive', 'enable interactive navigation mode')
305+
.action(async (options) => {
306+
// Handle interactive mode
307+
if (options.interactive) {
308+
await runInteractiveMode();
309+
return;
310+
}
311+
302312
let output;
303313
const sections = options.section;
304314

@@ -341,4 +351,297 @@ program
341351
}
342352
});
343353

354+
// Interactive mode function
355+
async function runInteractiveMode() {
356+
console.log(chalk.cyanBright.bold('\n🚀 Interactive Resume Navigator\n'));
357+
358+
while (true) {
359+
const { action } = await inquirer.prompt([
360+
{
361+
type: 'list',
362+
name: 'action',
363+
message: 'What would you like to do?',
364+
choices: [
365+
{ name: '📄 View Resume Sections', value: 'sections' },
366+
{ name: '📱 Generate QR Codes', value: 'qr' },
367+
{ name: '📋 Copy Contact Info', value: 'clipboard' },
368+
{ name: '💾 Export Resume', value: 'export' },
369+
{ name: '❌ Exit', value: 'exit' }
370+
]
371+
}
372+
]);
373+
374+
switch (action) {
375+
case 'sections':
376+
await navigateSections();
377+
break;
378+
case 'qr':
379+
await generateQRCodes();
380+
break;
381+
case 'clipboard':
382+
await copyToClipboard();
383+
break;
384+
case 'export':
385+
await exportResume();
386+
break;
387+
case 'exit':
388+
console.log(chalk.greenBright('\n👋 Thanks for using the interactive resume!\n'));
389+
return;
390+
}
391+
}
392+
}
393+
394+
// Navigate through resume sections
395+
async function navigateSections() {
396+
const sectionChoices = [
397+
{ name: '👤 Personal Info', value: 'personal' },
398+
{ name: '📝 Profile', value: 'profile' },
399+
{ name: '⚡ Tech Stack', value: 'techStack' },
400+
{ name: '💼 Experience', value: 'experience' },
401+
{ name: '🚀 Projects', value: 'projects' },
402+
{ name: '👥 Leadership', value: 'leadership' },
403+
{ name: '🌟 Open Source', value: 'openSource' },
404+
{ name: '🎓 Education', value: 'education' },
405+
{ name: '📄 Full Resume', value: 'full' },
406+
{ name: '⬅️ Back to Main Menu', value: 'back' }
407+
];
408+
409+
while (true) {
410+
const { section } = await inquirer.prompt([
411+
{
412+
type: 'list',
413+
name: 'section',
414+
message: 'Which section would you like to view?',
415+
choices: sectionChoices
416+
}
417+
]);
418+
419+
if (section === 'back') break;
420+
421+
console.log('\n' + '='.repeat(50));
422+
if (section === 'full') {
423+
console.log(formatColoredResume());
424+
} else {
425+
console.log(formatColoredResume([section]));
426+
}
427+
console.log('='.repeat(50) + '\n');
428+
429+
// Ask if user wants to continue viewing sections
430+
const { continueViewing } = await inquirer.prompt([
431+
{
432+
type: 'confirm',
433+
name: 'continueViewing',
434+
message: 'Would you like to view another section?',
435+
default: true
436+
}
437+
]);
438+
439+
if (!continueViewing) break;
440+
}
441+
}
442+
443+
// Generate QR codes for contact information
444+
async function generateQRCodes() {
445+
const qrChoices = [
446+
{ name: '📧 Email', value: 'email' },
447+
{ name: '📱 Phone', value: 'phone' },
448+
{ name: '💼 LinkedIn', value: 'linkedin' },
449+
{ name: '🐙 GitHub', value: 'github' },
450+
{ name: '🌐 Portfolio', value: 'portfolio' },
451+
{ name: '⬅️ Back to Main Menu', value: 'back' }
452+
];
453+
454+
while (true) {
455+
const { contact } = await inquirer.prompt([
456+
{
457+
type: 'list',
458+
name: 'contact',
459+
message: 'Generate QR code for which contact method?',
460+
choices: qrChoices
461+
}
462+
]);
463+
464+
if (contact === 'back') break;
465+
466+
let contactData = '';
467+
let contactLabel = '';
468+
469+
switch (contact) {
470+
case 'email':
471+
contactData = `mailto:${resumeData.personal.email.replace('📧 ', '')}`;
472+
contactLabel = 'Email';
473+
break;
474+
case 'phone':
475+
contactData = `tel:${resumeData.personal.phone.replace('📱 ', '')}`;
476+
contactLabel = 'Phone';
477+
break;
478+
case 'linkedin':
479+
contactData = resumeData.personal.linkedin.replace('🔗 ', '');
480+
contactLabel = 'LinkedIn';
481+
break;
482+
case 'github':
483+
contactData = resumeData.personal.github.replace('🐙 ', '');
484+
contactLabel = 'GitHub';
485+
break;
486+
case 'portfolio':
487+
contactData = resumeData.personal.portfolio.replace('🌐 ', '');
488+
contactLabel = 'Portfolio';
489+
break;
490+
}
491+
492+
try {
493+
console.log(`\n${chalk.cyanBright.bold(`QR Code for ${contactLabel}:`)}`);
494+
console.log(chalk.dim(`Data: ${contactData}\n`));
495+
496+
const qrString = await qrcode.toString(contactData, {
497+
type: 'terminal',
498+
small: true
499+
});
500+
501+
console.log(qrString);
502+
console.log(chalk.yellowBright('📱 Scan with your phone to access this contact info!\n'));
503+
504+
} catch (error) {
505+
console.error(chalk.red(`Error generating QR code: ${error.message}`));
506+
}
507+
508+
// Ask if user wants to generate another QR code
509+
const { continueQR } = await inquirer.prompt([
510+
{
511+
type: 'confirm',
512+
name: 'continueQR',
513+
message: 'Would you like to generate another QR code?',
514+
default: true
515+
}
516+
]);
517+
518+
if (!continueQR) break;
519+
}
520+
}
521+
522+
// Copy contact information to clipboard
523+
async function copyToClipboard() {
524+
const clipboardChoices = [
525+
{ name: '📧 Email Address', value: 'email' },
526+
{ name: '📱 Phone Number', value: 'phone' },
527+
{ name: '💼 LinkedIn URL', value: 'linkedin' },
528+
{ name: '🐙 GitHub URL', value: 'github' },
529+
{ name: '🌐 Portfolio URL', value: 'portfolio' },
530+
{ name: '📄 All Contact Info', value: 'all' },
531+
{ name: '⬅️ Back to Main Menu', value: 'back' }
532+
];
533+
534+
while (true) {
535+
const { contact } = await inquirer.prompt([
536+
{
537+
type: 'list',
538+
name: 'contact',
539+
message: 'What would you like to copy to clipboard?',
540+
choices: clipboardChoices
541+
}
542+
]);
543+
544+
if (contact === 'back') break;
545+
546+
let clipboardData = '';
547+
let contactLabel = '';
548+
549+
switch (contact) {
550+
case 'email':
551+
clipboardData = resumeData.personal.email.replace('📧 ', '');
552+
contactLabel = 'Email address';
553+
break;
554+
case 'phone':
555+
clipboardData = resumeData.personal.phone.replace('📱 ', '');
556+
contactLabel = 'Phone number';
557+
break;
558+
case 'linkedin':
559+
clipboardData = resumeData.personal.linkedin.replace('🔗 ', '');
560+
contactLabel = 'LinkedIn URL';
561+
break;
562+
case 'github':
563+
clipboardData = resumeData.personal.github.replace('🐙 ', '');
564+
contactLabel = 'GitHub URL';
565+
break;
566+
case 'portfolio':
567+
clipboardData = resumeData.personal.portfolio.replace('🌐 ', '');
568+
contactLabel = 'Portfolio URL';
569+
break;
570+
case 'all':
571+
clipboardData = `Email: ${resumeData.personal.email.replace('📧 ', '')}\nPhone: ${resumeData.personal.phone.replace('📱 ', '')}\nLinkedIn: ${resumeData.personal.linkedin.replace('🔗 ', '')}\nGitHub: ${resumeData.personal.github.replace('🐙 ', '')}\nPortfolio: ${resumeData.personal.portfolio.replace('🌐 ', '')}`;
572+
contactLabel = 'All contact information';
573+
break;
574+
}
575+
576+
try {
577+
await clipboardy.write(clipboardData);
578+
console.log(chalk.greenBright(`\n✅ ${contactLabel} copied to clipboard!`));
579+
console.log(chalk.dim(`Copied: ${clipboardData.split('\n')[0]}${clipboardData.includes('\n') ? '...' : ''}\n`));
580+
} catch (error) {
581+
console.error(chalk.red(`Error copying to clipboard: ${error.message}`));
582+
}
583+
584+
// Ask if user wants to copy something else
585+
const { continueCopy } = await inquirer.prompt([
586+
{
587+
type: 'confirm',
588+
name: 'continueCopy',
589+
message: 'Would you like to copy something else?',
590+
default: true
591+
}
592+
]);
593+
594+
if (!continueCopy) break;
595+
}
596+
}
597+
598+
// Export resume in different formats
599+
async function exportResume() {
600+
const { format } = await inquirer.prompt([
601+
{
602+
type: 'list',
603+
name: 'format',
604+
message: 'Which format would you like to export?',
605+
choices: [
606+
{ name: '🎨 Colored (Terminal)', value: 'colored' },
607+
{ name: '📝 Plain Text', value: 'plain' },
608+
{ name: '📊 JSON', value: 'json' }
609+
]
610+
}
611+
]);
612+
613+
const { filename } = await inquirer.prompt([
614+
{
615+
type: 'input',
616+
name: 'filename',
617+
message: 'Enter filename (without extension):',
618+
default: 'bharathkumar-resume'
619+
}
620+
]);
621+
622+
const extensions = { colored: 'txt', plain: 'txt', json: 'json' };
623+
const fullFilename = `${filename}.${extensions[format]}`;
624+
625+
let output;
626+
switch (format) {
627+
case 'json':
628+
output = formatJsonResume();
629+
break;
630+
case 'plain':
631+
output = formatPlainResume();
632+
break;
633+
case 'colored':
634+
default:
635+
output = formatColoredResume();
636+
break;
637+
}
638+
639+
try {
640+
fs.writeFileSync(fullFilename, output);
641+
console.log(chalk.greenBright(`\n✅ Resume exported to ${fullFilename}!\n`));
642+
} catch (error) {
643+
console.error(chalk.red(`Error exporting resume: ${error.message}`));
644+
}
645+
}
646+
344647
program.parse();

0 commit comments

Comments
 (0)