|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>The Optimal M4 Frontend Developer Setup | 2025</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap" rel="stylesheet"> |
|
<style> |
|
body { |
|
font-family: 'Inter', sans-serif; |
|
background-color: #F0F4F8; /* Light Blue-Gray Background */ |
|
} |
|
.chart-container { |
|
position: relative; |
|
width: 100%; |
|
max-width: 600px; |
|
margin-left: auto; |
|
margin-right: auto; |
|
height: 350px; |
|
max-height: 400px; |
|
} |
|
@media (max-width: 768px) { |
|
.chart-container { |
|
height: 300px; |
|
} |
|
} |
|
.kpi-card { |
|
background-color: #ffffff; |
|
border-radius: 0.75rem; |
|
padding: 1.5rem; |
|
text-align: center; |
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); |
|
transition: transform 0.3s ease, box-shadow 0.3s ease; |
|
} |
|
.kpi-card:hover { |
|
transform: translateY(-5px); |
|
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); |
|
} |
|
.flowchart-step { |
|
background-color: #FFFFFF; |
|
border: 2px solid #FF6B6B; /* Energetic Red */ |
|
color: #1A535C; /* Dark Teal */ |
|
} |
|
.flowchart-arrow { |
|
color: #FF6B6B; /* Energetic Red */ |
|
} |
|
.gemini-feature-card { |
|
background: linear-gradient(135deg, #F7FFF7, #EFFFFE); |
|
} |
|
</style> |
|
</head> |
|
<body class="text-[#1A535C]"> |
|
|
|
<div class="container mx-auto p-4 md:p-8 max-w-7xl"> |
|
|
|
<header class="text-center my-8 md:my-12"> |
|
<h1 class="text-4xl md:text-6xl font-black text-[#FF6B6B] tracking-tight">The Optimal Frontend Setup</h1> |
|
<h2 class="text-2xl md:text-4xl font-bold text-[#4ECDC4] mt-2">Apple Silicon M4 Edition | 2025</h2> |
|
<p class="max-w-3xl mx-auto mt-4 text-lg text-gray-600">A curated guide to building a high-performance, modern development environment on the latest Apple hardware, focusing on speed, efficiency, and a seamless workflow.</p> |
|
</header> |
|
|
|
<main> |
|
<section id="bedrock" class="mb-12 md:mb-20"> |
|
<h3 class="text-3xl font-bold text-center mb-8">The Bedrock: Core Command-Line Configuration</h3> |
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 text-center"> |
|
<div class="kpi-card"> |
|
<div class="text-5xl font-bold text-[#FF6B6B]">1</div> |
|
<h4 class="text-xl font-semibold mt-4 mb-2">Xcode Command Line Tools</h4> |
|
<p class="text-gray-600">The essential first step. Installs compilers and Git, preparing your Mac for development without the full Xcode IDE.</p> |
|
<code class="mt-4 inline-block bg-gray-100 text-sm p-2 rounded">xcode-select --install</code> |
|
</div> |
|
<div class="kpi-card"> |
|
<div class="text-5xl font-bold text-[#FF6B6B]">2</div> |
|
<h4 class="text-xl font-semibold mt-4 mb-2">Homebrew Package Manager</h4> |
|
<p class="text-gray-600">The "missing package manager for macOS." Manages all your tools in a clean, isolated `/opt/homebrew` directory.</p> |
|
<code class="mt-4 inline-block bg-gray-100 text-sm p-2 rounded">/bin/bash -c "$(curl...)"</code> |
|
</div> |
|
<div class="kpi-card"> |
|
<div class="text-5xl font-bold text-[#FF6B6B]">3</div> |
|
<h4 class="text-xl font-semibold mt-4 mb-2">Git Version Control</h4> |
|
<p class="text-gray-600">Install the latest version via Homebrew to ensure you have the most up-to-date features for version control.</p> |
|
<code class="mt-4 inline-block bg-gray-100 text-sm p-2 rounded">brew install git</code> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<section id="command-center" class="mb-12 md:mb-20"> |
|
<h3 class="text-3xl font-bold text-center mb-8">The Command Center: Terminal & Shell</h3> |
|
<div class="bg-white rounded-lg shadow-md p-6 md:p-8 mb-8"> |
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center"> |
|
<div> |
|
<h4 class="text-2xl font-bold text-[#FF6B6B] mb-4">Terminal Recommendation: Warp</h4> |
|
<p class="text-gray-600 mb-6">For a 2025 setup, Warp is the recommended terminal. Built in Rust, it's incredibly fast and rethinks the command line with an IDE-style editor, AI assistance, and collaborative features. It represents the future of developer command-line interaction.</p> |
|
<div class="kpi-card !bg-[#F7FFF7] border-l-4 border-[#4ECDC4]"> |
|
<h5 class="text-lg font-semibold text-[#1A535C]">Key Feature: AI Integration</h5> |
|
<p class="text-gray-600 mt-2">Generate commands from natural language and debug errors directly in your terminal, a massive productivity boost over traditional emulators.</p> |
|
</div> |
|
</div> |
|
<div class="chart-container h-80 md:h-96"> |
|
<canvas id="terminalComparisonChart"></canvas> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- Gemini API Feature --> |
|
<div class="gemini-feature-card rounded-lg shadow-md p-6 md:p-8 border-2 border-[#4ECDC4]"> |
|
<h4 class="text-2xl font-bold text-center text-[#1A535C] mb-4">✨ AI Command Generator</h4> |
|
<p class="text-center text-gray-600 mb-6">Describe what you want to do, and let Gemini generate the command for you.</p> |
|
<div class="max-w-2xl mx-auto"> |
|
<div class="flex flex-col sm:flex-row gap-2"> |
|
<input type="text" id="command-prompt-input" class="flex-grow p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-[#FF6B6B] focus:outline-none" placeholder="e.g., find files larger than 50MB"> |
|
<button id="generate-command-btn" class="bg-[#FF6B6B] text-white font-bold py-3 px-6 rounded-md hover:bg-red-500 transition-colors disabled:bg-gray-400">Generate</button> |
|
</div> |
|
<div id="command-output-container" class="mt-4 hidden"> |
|
<p class="font-semibold mb-2">Generated Command:</p> |
|
<div class="relative bg-gray-800 text-white p-4 rounded-md font-mono"> |
|
<code id="command-output"></code> |
|
<button id="copy-command-btn" class="absolute top-2 right-2 bg-gray-600 hover:bg-gray-500 text-white text-xs py-1 px-2 rounded">Copy</button> |
|
</div> |
|
</div> |
|
<div id="command-loader" class="text-center mt-4 hidden"> |
|
<p class="text-gray-600">Generating command...</p> |
|
</div> |
|
<div id="command-error" class="text-center mt-4 text-red-600 hidden"></div> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<section id="stack" class="mb-12 md:mb-20"> |
|
<h3 class="text-3xl font-bold text-center mb-8">Managing the Stack: Runtimes & Packages</h3> |
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> |
|
<div class="bg-white rounded-lg shadow-md p-6 md:p-8"> |
|
<h4 class="text-2xl font-bold text-[#FF6B6B] mb-4">Package Manager Showdown</h4> |
|
<p class="text-gray-600 mb-6">While `npm` is the default and `yarn` improved on it, `pnpm` is the technical leader for 2025 due to its superior speed and disk efficiency. It avoids duplicating packages by using a global store and linking files.</p> |
|
<div class="chart-container h-80 md:h-96"> |
|
<canvas id="packageManagerChart"></canvas> |
|
</div> |
|
</div> |
|
<div class="bg-white rounded-lg shadow-md p-6 md:p-8"> |
|
<h4 class="text-2xl font-bold text-[#FF6B6B] mb-4">Node.js Version Management</h4> |
|
<p class="text-gray-600 mb-6">Different projects require different Node.js versions. **NVM (Node Version Manager)** is the industry standard for installing and switching between versions seamlessly, preventing conflicts and ensuring project consistency.</p> |
|
<div class="space-y-4"> |
|
<div class="kpi-card !bg-[#F7FFF7] !p-4 text-left"> |
|
<h5 class="font-bold text-[#1A535C]">Install LTS Version</h5> |
|
<code class="text-sm text-gray-700">nvm install --lts</code> |
|
</div> |
|
<div class="kpi-card !bg-[#F7FFF7] !p-4 text-left"> |
|
<h5 class="font-bold text-[#1A535C]">Use Project-Specific Version</h5> |
|
<p class="text-sm text-gray-500 mb-1">Create a `.nvmrc` file in your project root, then run:</p> |
|
<code class="text-sm text-gray-700">nvm use</code> |
|
</div> |
|
<div class="kpi-card !bg-[#F7FFF7] !p-4 text-left"> |
|
<h5 class="font-bold text-[#1A535C]">Set Default Version</h5> |
|
<code class="text-sm text-gray-700">nvm alias default lts/*</code> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<section id="ide" class="mb-12 md:mb-20"> |
|
<h3 class="text-3xl font-bold text-center mb-8">The Workshop: Visual Studio Code</h3> |
|
<p class="max-w-3xl mx-auto text-center text-lg text-gray-600 mb-8">VS Code is the undisputed industry standard. The key is transforming it from a text editor into a development OS with essential extensions that automate quality control and supercharge your workflow.</p> |
|
<div class="bg-white rounded-lg shadow-md p-6 md:p-8"> |
|
<h4 class="text-2xl font-bold text-center text-[#FF6B6B] mb-6">Automated Code Quality Workflow</h4> |
|
<div class="flex flex-col md:flex-row items-center justify-center space-y-4 md:space-y-0 md:space-x-4"> |
|
<div class="flowchart-step p-4 rounded-lg text-center shadow"> |
|
<span class="text-2xl">📝</span> |
|
<h5 class="font-bold">1. Write Code</h5> |
|
<p class="text-sm">In VS Code</p> |
|
</div> |
|
<div class="flowchart-arrow text-4xl font-light transform md:-rotate-0 rotate-90">→</div> |
|
<div class="flowchart-step p-4 rounded-lg text-center shadow"> |
|
<span class="text-2xl">💾</span> |
|
<h5 class="font-bold">2. On Save: Prettier</h5> |
|
<p class="text-sm">Auto-formats code style</p> |
|
</div> |
|
<div class="flowchart-arrow text-4xl font-light transform md:-rotate-0 rotate-90">→</div> |
|
<div class="flowchart-step p-4 rounded-lg text-center shadow"> |
|
<span class="text-2xl">✨</span> |
|
<h5 class="font-bold">3. On Save: ESLint</h5> |
|
<p class="text-sm">Auto-fixes linting errors</p> |
|
</div> |
|
<div class="flowchart-arrow text-4xl font-light transform md:-rotate-0 rotate-90">→</div> |
|
<div class="flowchart-step p-4 rounded-lg text-center shadow bg-[#4ECDC4] !border-[#4ECDC4] text-white"> |
|
<span class="text-2xl">✅</span> |
|
<h5 class="font-bold">4. Result</h5> |
|
<p class="text-sm">Clean, consistent code</p> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mt-8"> |
|
<div class="kpi-card"> |
|
<h4 class="text-xl font-semibold mt-2 mb-2">Prettier</h4> |
|
<p class="text-gray-600">The opinionated code formatter. Ends all debates on style by automatically reformatting your code on save.</p> |
|
</div> |
|
<div class="kpi-card"> |
|
<h4 class="text-xl font-semibold mt-2 mb-2">ESLint</h4> |
|
<p class="text-gray-600">Finds and fixes problems in your JavaScript code, catching bugs and enforcing standards before they hit production.</p> |
|
</div> |
|
<div class="kpi-card"> |
|
<h4 class="text-xl font-semibold mt-2 mb-2">GitLens</h4> |
|
<p class="text-gray-600">Supercharges the built-in Git capabilities, providing authorship and history insights directly within your editor.</p> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<section id="auxiliary" class="mb-12 md:mb-20"> |
|
<h3 class="text-3xl font-bold text-center mb-8">Essential Auxiliary Tooling</h3> |
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> |
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
<h4 class="text-xl font-bold text-[#FF6B6B] mb-2">Git GUI Client</h4> |
|
<p class="text-gray-600 mb-4">For a high-level view of your repository. Start with the simplicity of **GitHub Desktop**, and graduate to **Sourcetree** for advanced features like interactive rebase.</p> |
|
<div class="chart-container h-64"> |
|
<canvas id="gitGuiChart"></canvas> |
|
</div> |
|
</div> |
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
<h4 class="text-xl font-bold text-[#FF6B6B] mb-2">API Client</h4> |
|
<p class="text-gray-600 mb-4">For testing and debugging APIs. **Bruno** is the top 2025 pick for its local-first, privacy-focused approach. **Postman** remains the standard for enterprise collaboration.</p> |
|
<div class="chart-container h-64"> |
|
<canvas id="apiClientChart"></canvas> |
|
</div> |
|
</div> |
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
<h4 class="text-xl font-bold text-[#FF6B6B] mb-2">Containerization</h4> |
|
<p class="text-gray-600 mb-4">**Docker Desktop** provides consistent, isolated environments for databases or backend services, running with native performance on Apple Silicon.</p> |
|
<div class="text-center pt-8"> |
|
<span class="text-8xl">🐳</span> |
|
<p class="font-bold text-lg mt-4 text-[#1A535C]">Docker for Mac</p> |
|
<p class="text-gray-500">Native `arm64` performance.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</section> |
|
</main> |
|
|
|
<footer class="text-center mt-12 md:mt-20 py-8 border-t border-gray-300"> |
|
<p class="text-gray-600">Infographic based on the "Optimal Frontend Development Environment for Apple Silicon (M4) in 2025" report.</p> |
|
<p class="text-sm text-gray-500 mt-2">Built with Tailwind CSS & Chart.js. Enhanced with the Gemini API.</p> |
|
</footer> |
|
</div> |
|
|
|
<script> |
|
const energeticPalette = { |
|
red: '#FF6B6B', |
|
teal: '#4ECDC4', |
|
darkTeal: '#1A535C', |
|
yellow: '#FFE66D', |
|
background: '#F7FFF7', |
|
gray: '#6c757d' |
|
}; |
|
|
|
const tooltipPlugin = { |
|
tooltip: { |
|
callbacks: { |
|
title: function(tooltipItems) { |
|
const item = tooltipItems[0]; |
|
let label = item.chart.data.labels[item.dataIndex]; |
|
if (Array.isArray(label)) { |
|
return label.join(' '); |
|
} else { |
|
return label; |
|
} |
|
} |
|
} |
|
} |
|
}; |
|
|
|
function wrapLabel(str, maxWidth) { |
|
if (str.length <= maxWidth) { |
|
return str; |
|
} |
|
const words = str.split(' '); |
|
let lines = []; |
|
let currentLine = words[0]; |
|
|
|
for (let i = 1; i < words.length; i++) { |
|
if ((currentLine + ' ' + words[i]).length > maxWidth) { |
|
lines.push(currentLine); |
|
currentLine = words[i]; |
|
} else { |
|
currentLine += ' ' + words[i]; |
|
} |
|
} |
|
lines.push(currentLine); |
|
return lines; |
|
} |
|
|
|
new Chart(document.getElementById('terminalComparisonChart'), { |
|
type: 'bar', |
|
data: { |
|
labels: ['Performance', 'AI Integration', 'Modern Text Editing', 'Collaboration', wrapLabel('Out-of-the-box Completions', 16)], |
|
datasets: [{ |
|
label: 'Warp (Recommended)', |
|
data: [95, 90, 95, 85, 90], |
|
backgroundColor: energeticPalette.red, |
|
borderColor: energeticPalette.red, |
|
borderWidth: 1 |
|
}, { |
|
label: 'iTerm2', |
|
data: [75, 40, 30, 20, 30], |
|
backgroundColor: energeticPalette.teal, |
|
borderColor: energeticPalette.teal, |
|
borderWidth: 1 |
|
}] |
|
}, |
|
options: { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
indexAxis: 'y', |
|
scales: { |
|
x: { |
|
beginAtZero: true, |
|
max: 100, |
|
grid: { color: 'rgba(0,0,0,0.05)' } |
|
}, |
|
y: { |
|
grid: { display: false } |
|
} |
|
}, |
|
plugins: { |
|
...tooltipPlugin, |
|
title: { display: true, text: 'Feature Score (Warp vs. iTerm2)', font: { size: 16 }, color: energeticPalette.darkTeal }, |
|
legend: { position: 'bottom' } |
|
} |
|
} |
|
}); |
|
|
|
new Chart(document.getElementById('packageManagerChart'), { |
|
type: 'radar', |
|
data: { |
|
labels: ['Speed', 'Disk Efficiency', 'Monorepo Support', wrapLabel('Prevents Phantom Dependencies', 16)], |
|
datasets: [{ |
|
label: 'pnpm (Recommended)', |
|
data: [95, 100, 90, 100], |
|
backgroundColor: 'rgba(255, 107, 107, 0.4)', |
|
borderColor: energeticPalette.red, |
|
pointBackgroundColor: energeticPalette.red, |
|
pointBorderColor: '#fff', |
|
pointHoverBackgroundColor: '#fff', |
|
pointHoverBorderColor: energeticPalette.red |
|
}, { |
|
label: 'Yarn', |
|
data: [80, 50, 85, 50], |
|
backgroundColor: 'rgba(78, 205, 196, 0.4)', |
|
borderColor: energeticPalette.teal, |
|
pointBackgroundColor: energeticPalette.teal, |
|
pointBorderColor: '#fff', |
|
pointHoverBackgroundColor: '#fff', |
|
pointHoverBorderColor: energeticPalette.teal |
|
}, { |
|
label: 'npm', |
|
data: [60, 20, 70, 40], |
|
backgroundColor: 'rgba(255, 230, 109, 0.4)', |
|
borderColor: energeticPalette.yellow, |
|
pointBackgroundColor: energeticPalette.yellow, |
|
pointBorderColor: '#fff', |
|
pointHoverBackgroundColor: '#fff', |
|
pointHoverBorderColor: energeticPalette.yellow |
|
}] |
|
}, |
|
options: { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
scales: { |
|
r: { |
|
angleLines: { color: 'rgba(0,0,0,0.1)' }, |
|
grid: { color: 'rgba(0,0,0,0.1)' }, |
|
pointLabels: { font: { size: 12 }, color: energeticPalette.darkTeal }, |
|
ticks: { backdropColor: 'transparent', color: energeticPalette.gray }, |
|
suggestedMin: 0, |
|
suggestedMax: 100 |
|
} |
|
}, |
|
plugins: { |
|
...tooltipPlugin, |
|
title: { display: true, text: 'Relative Strengths', font: { size: 16 }, color: energeticPalette.darkTeal }, |
|
legend: { position: 'bottom' } |
|
} |
|
} |
|
}); |
|
|
|
new Chart(document.getElementById('gitGuiChart'), { |
|
type: 'doughnut', |
|
data: { |
|
labels: ['Beginner Friendly (GitHub Desktop)', 'Advanced Features (Sourcetree)'], |
|
datasets: [{ |
|
data: [65, 35], |
|
backgroundColor: [energeticPalette.teal, energeticPalette.red], |
|
borderColor: '#F0F4F8', |
|
borderWidth: 4 |
|
}] |
|
}, |
|
options: { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
plugins: { |
|
...tooltipPlugin, |
|
title: { display: true, text: 'Recommended Usage Focus', font: { size: 14 }, color: energeticPalette.darkTeal }, |
|
legend: { position: 'bottom' } |
|
} |
|
} |
|
}); |
|
|
|
new Chart(document.getElementById('apiClientChart'), { |
|
type: 'pie', |
|
data: { |
|
labels: ['Local & Privacy-Focused (Bruno)', 'Enterprise & Collaboration (Postman)'], |
|
datasets: [{ |
|
data: [55, 45], |
|
backgroundColor: [energeticPalette.red, energeticPalette.yellow], |
|
borderColor: '#F0F4F8', |
|
borderWidth: 4 |
|
}] |
|
}, |
|
options: { |
|
responsive: true, |
|
maintainAspectRatio: false, |
|
plugins: { |
|
...tooltipPlugin, |
|
title: { display: true, text: 'Choosing Based on Need', font: { size: 14 }, color: energeticPalette.darkTeal }, |
|
legend: { position: 'bottom' } |
|
} |
|
} |
|
}); |
|
|
|
// Gemini API Feature Logic |
|
const commandPromptInput = document.getElementById('command-prompt-input'); |
|
const generateBtn = document.getElementById('generate-command-btn'); |
|
const commandOutputContainer = document.getElementById('command-output-container'); |
|
const commandOutput = document.getElementById('command-output'); |
|
const copyBtn = document.getElementById('copy-command-btn'); |
|
const loader = document.getElementById('command-loader'); |
|
const errorContainer = document.getElementById('command-error'); |
|
|
|
const apiKey = ""; // API key will be automatically provided by the environment. |
|
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`; |
|
|
|
async function generateCommand() { |
|
const userInput = commandPromptInput.value.trim(); |
|
if (!userInput) { |
|
errorContainer.textContent = 'Please enter a description.'; |
|
errorContainer.classList.remove('hidden'); |
|
return; |
|
} |
|
|
|
generateBtn.disabled = true; |
|
loader.classList.remove('hidden'); |
|
commandOutputContainer.classList.add('hidden'); |
|
errorContainer.classList.add('hidden'); |
|
|
|
const prompt = `You are an expert command-line assistant for macOS (zsh/bash). Based on the following user request, provide only the single, most appropriate terminal command. Do not add any explanation, preamble, or markdown formatting like \`\`\`bash. Just the raw command text. User request: "${userInput}"`; |
|
|
|
const payload = { |
|
contents: [{ |
|
parts: [{ text: prompt }] |
|
}] |
|
}; |
|
|
|
try { |
|
const response = await fetchWithRetry(apiUrl, { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify(payload) |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(`API error: ${response.statusText}`); |
|
} |
|
|
|
const result = await response.json(); |
|
|
|
const text = result?.candidates?.[0]?.content?.parts?.[0]?.text; |
|
|
|
if (text) { |
|
commandOutput.textContent = text.trim(); |
|
commandOutputContainer.classList.remove('hidden'); |
|
} else { |
|
throw new Error('Failed to generate command. The model returned an empty response.'); |
|
} |
|
|
|
} catch (err) { |
|
errorContainer.textContent = `Error: ${err.message}`; |
|
errorContainer.classList.remove('hidden'); |
|
} finally { |
|
generateBtn.disabled = false; |
|
loader.classList.add('hidden'); |
|
} |
|
} |
|
|
|
async function fetchWithRetry(url, options, retries = 3, delay = 1000) { |
|
for (let i = 0; i < retries; i++) { |
|
try { |
|
const response = await fetch(url, options); |
|
if (response.status !== 429) { // Not a rate limiting error |
|
return response; |
|
} |
|
} catch (error) { |
|
if (i === retries - 1) throw error; |
|
} |
|
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i))); |
|
} |
|
throw new Error('Max retries reached'); |
|
} |
|
|
|
function copyToClipboard() { |
|
const textToCopy = commandOutput.textContent; |
|
const textArea = document.createElement('textarea'); |
|
textArea.value = textToCopy; |
|
document.body.appendChild(textArea); |
|
textArea.select(); |
|
try { |
|
document.execCommand('copy'); |
|
copyBtn.textContent = 'Copied!'; |
|
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 2000); |
|
} catch (err) { |
|
console.error('Failed to copy text: ', err); |
|
copyBtn.textContent = 'Failed!'; |
|
setTimeout(() => { copyBtn.textContent = 'Copy'; }, 2000); |
|
} |
|
document.body.removeChild(textArea); |
|
} |
|
|
|
generateBtn.addEventListener('click', generateCommand); |
|
copyBtn.addEventListener('click', copyToClipboard); |
|
commandPromptInput.addEventListener('keydown', (e) => { |
|
if (e.key === 'Enter') { |
|
generateCommand(); |
|
} |
|
}); |
|
|
|
</script> |
|
</body> |
|
</html> |