UNPKG

wylink

Version:

Modern ve özelleştirilebilir link portal (linktree tarzı) oluşturmak için CLI aracı

590 lines (520 loc) 19.5 kB
#!/usr/bin/env node const program = require('commander'); const chalk = require('chalk'); const figlet = require('figlet'); const fs = require('fs-extra'); const path = require('path'); const inquirer = require('inquirer'); // Welcome message console.log( chalk.cyan( figlet.textSync('Wylink', { horizontalLayout: 'full' }) ) ); // Get package.json version const packageJson = require('../package.json'); program .version(packageJson.version) .description('A CLI to create your own wylink portal (linktree style website)'); program .command('create') .description('Create a new wylink portal') .action(async () => { const answers = await inquirer.prompt([ { type: 'input', name: 'name', message: 'Enter your name:', default: 'İsim Soyisim' }, { type: 'input', name: 'title', message: 'Enter your title/profession:', default: 'Frontend Geliştirici & UI/UX Tasarımcı' }, { type: 'input', name: 'outputDir', message: 'Where should we create your wylink portal?', default: './my-wylink' }, { type: 'input', name: 'profileImage', message: 'Enter your profile image URL (or leave default for placeholder):', default: 'https://i.pravatar.cc/150?img=3' } ]); try { await createLinkPortal(answers); console.log(chalk.green(`✅ Wylink portal created successfully at ${answers.outputDir}`)); console.log(chalk.yellow('To add links, run:')); console.log(chalk.cyan(` npx wylink add-link --dir ${answers.outputDir}`)); } catch (error) { console.error(chalk.red('Error creating wylink portal:'), error); } }); program .command('add-link') .description('Add a new link to your wylink portal') .option('-d, --dir <directory>', 'Directory of your wylink portal', './my-wylink') .action(async (options) => { const answers = await inquirer.prompt([ { type: 'input', name: 'title', message: 'Enter link title:', validate: input => input.trim() !== '' ? true : 'Title is required' }, { type: 'input', name: 'url', message: 'Enter link URL:', validate: input => { const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/; return urlPattern.test(input) || input.startsWith('mailto:') ? true : 'Please enter a valid URL'; } }, { type: 'input', name: 'description', message: 'Enter link description:', default: '' }, { type: 'list', name: 'icon', message: 'Choose an icon:', choices: [ { name: 'Briefcase (Portfolio)', value: 'fas fa-briefcase' }, { name: 'LinkedIn', value: 'fab fa-linkedin' }, { name: 'GitHub', value: 'fab fa-github' }, { name: 'YouTube', value: 'fab fa-youtube' }, { name: 'Blog/RSS', value: 'fas fa-rss' }, { name: 'Email', value: 'fas fa-envelope' }, { name: 'Twitter', value: 'fab fa-twitter' }, { name: 'Instagram', value: 'fab fa-instagram' }, { name: 'Facebook', value: 'fab fa-facebook' }, { name: 'Website', value: 'fas fa-globe' } ] } ]); try { await addLink(options.dir, answers); console.log(chalk.green('✅ Link added successfully!')); } catch (error) { console.error(chalk.red('Error adding link:'), error); } }); program.parse(process.argv); // If no arguments, show help if (!process.argv.slice(2).length) { program.outputHelp(); } // Function to create the basic link portal async function createLinkPortal(answers) { const { name, title, outputDir, profileImage } = answers; // Create directory if it doesn't exist await fs.ensureDir(outputDir); // Copy template files const templatesDir = path.join(__dirname, '../templates'); // Create data.js with empty links array const dataJs = `// Link data const linkData = [];`; await fs.writeFile(path.join(outputDir, 'data.js'), dataJs); // Create index.html const indexHtml = getIndexHtmlTemplate(name, title, profileImage); await fs.writeFile(path.join(outputDir, 'index.html'), indexHtml); // Create script.js const scriptJs = getScriptJsTemplate(); await fs.writeFile(path.join(outputDir, 'script.js'), scriptJs); } // Function to add a new link async function addLink(dir, link) { const dataJsPath = path.join(dir, 'data.js'); // Check if data.js exists if (!await fs.pathExists(dataJsPath)) { throw new Error(`Could not find data.js in ${dir}. Make sure the directory is correct.`); } // Read existing data.js const dataJsContent = await fs.readFile(dataJsPath, 'utf-8'); // Parse existing links let links = []; try { // Extract the array from the file content const arrayMatch = dataJsContent.match(/const linkData = (\[[\s\S]*\]);/); if (arrayMatch && arrayMatch[1]) { // Evaluate the array string (safer than using eval) links = JSON.parse(arrayMatch[1].replace(/'/g, '"') .replace(/(\w+):/g, '"$1":') .replace(/,(\s*[\]}])/g, '$1')); } } catch (error) { console.warn(chalk.yellow('Could not parse existing links, creating new array')); } // Add new link const newLink = { id: links.length > 0 ? Math.max(...links.map(l => l.id)) + 1 : 1, title: link.title, description: link.description, url: link.url, icon: link.icon }; links.push(newLink); // Format links for output const linksFormatted = links.map(l => { return ` { id: ${l.id}, title: "${l.title}", description: "${l.description}", url: "${l.url}", icon: "${l.icon}" }`; }).join(",\n"); // Create new data.js content const newDataJsContent = `// Link data const linkData = [ ${linksFormatted} ];`; // Write updated data.js await fs.writeFile(dataJsPath, newDataJsContent); } // Template for index.html function getIndexHtmlTemplate(name, title, profileImage) { return `<!DOCTYPE html> <html lang="tr"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${name} - Wylink</title> <!-- Tailwind CSS from CDN --> <script src="https://cdn.tailwindcss.com"></script> <!-- Font Awesome --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <!-- Particles.js --> <script src="https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js"></script> <!-- Configure Tailwind --> <script> tailwind.config = { theme: { extend: { colors: { 'primary-bg': '#0D1321', 'secondary-bg': '#1D2D44', 'card-bg': '#3E5C76', 'highlight': '#748CAB', 'text-color': '#F0EBD8', } } } } </script> <!-- Google Font --> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <!-- Custom Styles --> <style> body { font-family: 'Poppins', sans-serif; } #particles-js { position: absolute; width: 100%; height: 100%; top: 0; left: 0; z-index: -1; } .glass-effect { background-color: rgba(62, 92, 118, 0.2); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); } .gradient-hover::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(45deg, transparent, #748CAB, transparent); opacity: 0; transition: opacity 0.3s ease; z-index: -1; } .gradient-hover:hover::before { opacity: 0.2; } .reveal { opacity: 0; transform: translateY(30px); transition: all 0.8s ease; } .revealed { opacity: 1; transform: translateY(0); } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .rotate-on-hover { transition: all 0.3s ease; } .link-hover:hover .rotate-on-hover { animation: rotate 0.3s forwards; } /* Custom Scrollbar Styles */ /* For Webkit browsers (Chrome, Safari, newer versions of Opera) */ ::-webkit-scrollbar { width: 10px; } ::-webkit-scrollbar-track { background: #1D2D44; border-radius: 10px; } ::-webkit-scrollbar-thumb { background: #748CAB; border-radius: 10px; border: 2px solid #1D2D44; } ::-webkit-scrollbar-thumb:hover { background: #8da6c2; } /* For Firefox */ * { scrollbar-width: thin; scrollbar-color: #748CAB #1D2D44; } /* For Edge and IE */ body { -ms-overflow-style: none; } </style> </head> <body class="bg-primary-bg text-text-color min-h-screen overflow-x-hidden relative"> <div id="particles-js"></div> <div class="max-w-3xl mx-auto p-4 md:p-8 relative z-10"> <header class="flex flex-col items-center mb-8 text-center"> <div class="w-24 h-24 md:w-30 md:h-30 rounded-full overflow-hidden mb-4 border-3 border-highlight shadow-lg transition-all duration-300 ease-in-out hover:scale-105 hover:shadow-xl reveal"> <img src="${profileImage}" alt="${name}" class="w-full h-full object-cover"> </div> <h1 class="text-3xl md:text-4xl font-bold mb-2 text-text-color reveal">${name}</h1> <p class="text-base md:text-lg font-light mb-6 text-highlight reveal">${title}</p> <div class="flex gap-4 mb-8 reveal"> <a href="#" class="w-10 h-10 rounded-full bg-secondary-bg flex items-center justify-center text-xl shadow-md transition-all duration-300 ease-in-out hover:-translate-y-1 hover:bg-highlight hover:text-primary-bg"> <i class="fab fa-twitter"></i> </a> <a href="#" class="w-10 h-10 rounded-full bg-secondary-bg flex items-center justify-center text-xl shadow-md transition-all duration-300 ease-in-out hover:-translate-y-1 hover:bg-highlight hover:text-primary-bg"> <i class="fab fa-instagram"></i> </a> <a href="#" class="w-10 h-10 rounded-full bg-secondary-bg flex items-center justify-center text-xl shadow-md transition-all duration-300 ease-in-out hover:-translate-y-1 hover:bg-highlight hover:text-primary-bg"> <i class="fab fa-github"></i> </a> <a href="#" class="w-10 h-10 rounded-full bg-secondary-bg flex items-center justify-center text-xl shadow-md transition-all duration-300 ease-in-out hover:-translate-y-1 hover:bg-highlight hover:text-primary-bg"> <i class="fab fa-linkedin"></i> </a> </div> </header> <main> <section class="flex flex-col gap-4 mb-8" id="links-container"> <!-- Links will be dynamically loaded here from data.js --> </section> </main> <footer class="text-center py-4 text-sm text-text-color/70 border-t border-white/10 mt-8 reveal"> <p>© 2025 ${name} | Created with <a href="https://github.com/wyltre/wylink" class="hover:text-highlight transition-colors duration-300">Wylink</a></p> </footer> </div> <script src="data.js"></script> <script src="script.js"></script> </body> </html>`; } // Template for script.js function getScriptJsTemplate() { return `document.addEventListener('DOMContentLoaded', () => { // Initialize the particles.js initParticles(); // Load the links dynamically loadLinks(); // Initialize scroll reveal initScrollReveal(); }); // Initialize particles.js function initParticles() { particlesJS('particles-js', { "particles": { "number": { "value": 80, "density": { "enable": true, "value_area": 800 } }, "color": { "value": "#748CAB" }, "shape": { "type": "circle", "stroke": { "width": 0, "color": "#000000" }, "polygon": { "nb_sides": 5 } }, "opacity": { "value": 0.5, "random": false, "anim": { "enable": false, "speed": 1, "opacity_min": 0.1, "sync": false } }, "size": { "value": 3, "random": true, "anim": { "enable": false, "speed": 40, "size_min": 0.1, "sync": false } }, "line_linked": { "enable": true, "distance": 150, "color": "#3E5C76", "opacity": 0.4, "width": 1 }, "move": { "enable": true, "speed": 2, "direction": "none", "random": false, "straight": false, "out_mode": "out", "bounce": false, "attract": { "enable": false, "rotateX": 600, "rotateY": 1200 } } }, "interactivity": { "detect_on": "canvas", "events": { "onhover": { "enable": true, "mode": "grab" }, "onclick": { "enable": true, "mode": "push" }, "resize": true }, "modes": { "grab": { "distance": 140, "line_linked": { "opacity": 1 } }, "bubble": { "distance": 400, "size": 40, "duration": 2, "opacity": 8, "speed": 3 }, "repulse": { "distance": 200, "duration": 0.4 }, "push": { "particles_nb": 4 }, "remove": { "particles_nb": 2 } } }, "retina_detect": true }); } // Load the links dynamically function loadLinks() { const linksContainer = document.getElementById('links-container'); // Clear container first linksContainer.innerHTML = ''; // Check if linkData exists and has items if (!linkData || linkData.length === 0) { const emptyMessage = document.createElement('div'); emptyMessage.className = 'glass-effect rounded-lg p-6 text-center'; emptyMessage.innerHTML = '<p>Henüz bağlantı eklenmemiş. Terminal üzerinden bağlantı ekleyebilirsiniz.</p>'; linksContainer.appendChild(emptyMessage); return; } linkData.forEach(link => { // Create elements const linkCard = document.createElement('a'); linkCard.href = link.url; linkCard.className = 'glass-effect link-hover gradient-hover rounded-lg p-4 flex items-center relative overflow-hidden shadow-lg transition-all duration-300 ease-in-out hover:-translate-y-1 hover:scale-[1.02] hover:shadow-xl reveal'; linkCard.target = '_blank'; linkCard.rel = 'noopener noreferrer'; const linkIcon = document.createElement('div'); linkIcon.className = 'w-10 h-10 rounded-full bg-secondary-bg flex items-center justify-center text-lg mr-4 flex-shrink-0 rotate-on-hover'; const icon = document.createElement('i'); icon.className = link.icon; const linkContent = document.createElement('div'); linkContent.className = 'flex-grow'; const linkTitle = document.createElement('h3'); linkTitle.className = 'text-lg font-semibold mb-1'; linkTitle.textContent = link.title; const linkDescription = document.createElement('p'); linkDescription.className = 'text-sm text-text-color/80'; linkDescription.textContent = link.description; // Assemble the link card linkIcon.appendChild(icon); linkContent.appendChild(linkTitle); linkContent.appendChild(linkDescription); linkCard.appendChild(linkIcon); linkCard.appendChild(linkContent); // Add to container linksContainer.appendChild(linkCard); }); } // Initialize scroll reveal function initScrollReveal() { // The function to reveal elements when they are in viewport function revealElements() { const elements = document.querySelectorAll('.reveal'); const windowHeight = window.innerHeight; elements.forEach(element => { const elementTop = element.getBoundingClientRect().top; const elementVisible = 150; // The element will be revealed when it's 150px from the viewport if (elementTop < windowHeight - elementVisible) { element.classList.add('revealed'); } else { element.classList.remove('revealed'); } }); } // Initial check on page load revealElements(); // Add scroll event listener window.addEventListener('scroll', revealElements); }`; }