From 6e82a552190ec12d168c60b06534109f138d7266 Mon Sep 17 00:00:00 2001 From: jassim Date: Tue, 4 Nov 2025 17:40:16 +0530 Subject: [PATCH] initial-commit --- .gitignore | 7 + API_QUICK_REFERENCE.md | 286 ++++ README.md | 351 +++++ SIMPLIFIED_FLOW.md | 201 +++ index.html | 13 + package-lock.json | 1909 +++++++++++++++++++++++ package.json | 20 + semrussh.postman_collection.json | 412 +++++ src/App.css | 181 +++ src/App.jsx | 98 ++ src/components/ComprehensiveMetrics.css | 471 ++++++ src/components/ComprehensiveMetrics.jsx | 261 ++++ src/components/KeywordForm.css | 169 ++ src/components/KeywordForm.jsx | 212 +++ src/components/KeywordResults.css | 230 +++ src/components/KeywordResults.jsx | 158 ++ src/components/SEOMaturityAnalysis.css | 881 +++++++++++ src/components/SEOMaturityAnalysis.jsx | 406 +++++ src/components/UnifiedAnalysis.css | 1435 +++++++++++++++++ src/components/UnifiedAnalysis.jsx | 727 +++++++++ src/index.css | 22 + src/main.jsx | 11 + src/services/semrushApi.js | 1344 ++++++++++++++++ vite.config.js | 46 + 24 files changed, 9851 insertions(+) create mode 100644 .gitignore create mode 100644 API_QUICK_REFERENCE.md create mode 100644 README.md create mode 100644 SIMPLIFIED_FLOW.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 semrussh.postman_collection.json create mode 100644 src/App.css create mode 100644 src/App.jsx create mode 100644 src/components/ComprehensiveMetrics.css create mode 100644 src/components/ComprehensiveMetrics.jsx create mode 100644 src/components/KeywordForm.css create mode 100644 src/components/KeywordForm.jsx create mode 100644 src/components/KeywordResults.css create mode 100644 src/components/KeywordResults.jsx create mode 100644 src/components/SEOMaturityAnalysis.css create mode 100644 src/components/SEOMaturityAnalysis.jsx create mode 100644 src/components/UnifiedAnalysis.css create mode 100644 src/components/UnifiedAnalysis.jsx create mode 100644 src/index.css create mode 100644 src/main.jsx create mode 100644 src/services/semrushApi.js create mode 100644 vite.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0cc2c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +dist +.DS_Store +*.log +.env +.env.local + diff --git a/API_QUICK_REFERENCE.md b/API_QUICK_REFERENCE.md new file mode 100644 index 0000000..a96be63 --- /dev/null +++ b/API_QUICK_REFERENCE.md @@ -0,0 +1,286 @@ +# SEMrush API Quick Reference + +## ⚠️ IMPORTANT: Costs Are ESTIMATED + +**All costs below are ESTIMATES. Verify against your actual usage:** +- Check actual consumption: [https://www.semrush.com/api-units/](https://www.semrush.com/api-units/) +- Run calibration after first analysis (see below) +- Update `API_UNIT_COSTS` in `semrushApi.js` if needed + +### ✅ Verified from Documentation: +- **phrase_this (Keyword Overview)**: 10 units/line +- **Batch Keyword Overview**: 10 units/line +- **Keyword Difficulty (phrase_kdi)**: 50 units/line +- **domain_organic**: 10 units/line + +### ⚠️ Estimated (needs verification): +- domain_domains, domain_ranks, backlinks_* endpoints + +--- + +## 🚀 Quick Start + +### Single Analysis Cost (ESTIMATED) +``` +1 client + 3 competitors + 1000 keyword limit = ~$0.37 per analysis +(Verify with actual usage after first run!) +``` + +### API Unit Costs Cheat Sheet +``` +✅ VERIFIED: +phrase_this: 10 units/line (Keyword Overview) +domain_organic: 10 units/line (Organic keywords) +phrase_kdi: 50 units/line (Keyword Difficulty) + +⚠️ ESTIMATED (verify with actual usage): +domain_domains: 10 units/line +domain_ranks: 10 units/domain +backlinks_overview: 10 units/domain +backlinks_refdomains: 10 units/line +``` + +## 📊 Cost Calculator + +### By Display Limit +| Limit | Cost/Analysis | +|-------|---------------| +| 100 | $0.04 | +| 500 | $0.20 | +| 1,000 | $0.37 | +| 2,000 | $0.72 | +| 5,000 | $1.77 | + +### By Number of Analyses +| Analyses | API Cost | Total w/ Plan | +|----------|----------|---------------| +| 1 | $0.37 | $500.32 | +| 10 | $3.74 | $503.69 | +| 100 | $37.40 | $537.35 | +| 1,000 | $374.00 | $873.95 | + +## 🎯 First-Time Setup: Calibrate Costs + +### 1. Run Small Test Analysis +```javascript +const result = await fetchUnifiedAnalysis({ + clientUrl: 'mysite.com', + competitors: ['competitor1.com'], + database: 'us', + apiKey: process.env.SEMRUSH_API_KEY, + displayLimit: 100 // Keep it small for testing +}) + +console.log('Estimated units:', result.apiUsage.totalUnits) +``` + +### 2. Check Actual Usage +Go to [https://www.semrush.com/api-units/](https://www.semrush.com/api-units/) and check API Query Log + +### 3. Calibrate +```javascript +import { calibrateApiCosts } from './services/semrushApi' + +const actualUnits = 856 // From SEMrush dashboard +const calibration = calibrateApiCosts(actualUnits, { + linesReturned: 85 +}) + +console.log(`Accuracy: ${calibration.accuracyPercent}%`) +``` + +### 4. Update if Needed +If accuracy < 90%, update `API_UNIT_COSTS` in `semrushApi.js` + +--- + +## 🔧 Usage Examples + +### Basic Analysis +```javascript +import { fetchUnifiedAnalysis, getApiUnitReport } from './services/semrushApi' + +const result = await fetchUnifiedAnalysis({ + clientUrl: 'mysite.com', + competitors: ['competitor1.com', 'competitor2.com'], + database: 'us', + apiKey: process.env.SEMRUSH_API_KEY, + displayLimit: 1000 +}) + +// Check API usage +console.log('Units used:', result.apiUsage.totalUnits) +console.log('Cost:', result.apiUsage.totalCost) +``` + +### Estimate Before Running +```javascript +import { estimateApiUnits } from './services/semrushApi' + +const estimate = estimateApiUnits({ + clientUrl: 'mysite.com', + competitors: ['comp1.com', 'comp2.com', 'comp3.com'], + displayLimit: 1000 +}) + +console.log(`Estimated cost: ${estimate.total.cost}`) +console.log(`Cost per 100 analyses: ${estimate.total.per100Analyses}`) +``` + +### Check Single Keyword Intent +```javascript +import { fetchPhraseIntent } from './services/semrushApi' + +const intent = await fetchPhraseIntent('cloud security tips', 'us', apiKey) +console.log(intent.intentLabel) // "Informational" +console.log('Cost: $0.005 (100 units)') +``` + +## ⚠️ Common Pitfalls + +### 1. Use Built-in Intent Filter (More Efficient) +```javascript +// ✅ BEST - Intent filter built into domain_domains (no extra cost) +const result = await fetchDomainVsDomain(...) // Intent=1 filter included + +// ⚠️ WORKS BUT REDUNDANT - phrase_this costs 10 units/keyword ($0.50 for 1000) +const keywords = await filterInformationalKeywords(keywords, 'us', apiKey) +// Only use if you need to double-validate Intent values +``` + +### 2. Don't Set Display Limit Too High +```javascript +// ❌ BAD - Costs $1.77 per analysis +displayLimit: 5000 + +// ✅ GOOD - Costs $0.37 per analysis, usually sufficient +displayLimit: 1000 +``` + +### 3. Don't Forget Business Plan Cost +``` +API units alone: $0.37 per analysis +But monthly cost: $499.95 + API units +First analysis: $500.32 total +``` + +## 📈 Optimization Tips + +### 1. Use Intent Filter (Built-in) +Reduces results by ~30% = saves 3,000 units per analysis + +### 2. Batch Multiple Clients +Run 100 analyses together instead of one-off + +### 3. Cache Results +Store results for 24-48 hours to avoid re-analysis + +### 4. Optimize Display Limit +Start with 500, increase only if needed + +### 5. Monitor Monthly Usage +Set alerts at 80% of purchased units + +## 🎯 Break-Even Analysis + +### Monthly Business Plan: $499.95 + +If you run: +- **1 analysis/month**: $500.32 total +- **100 analyses/month**: $537.35 total ($5.37 per analysis) +- **1,000 analyses/month**: $873.95 total ($0.87 per analysis) + +### Per-Analysis Cost Breakdown +``` +Fixed: $499.95 / analyses_per_month +Variable: ~$0.37 per analysis + +1 analysis: $500.32 per analysis +10 analyses: $50.37 per analysis +100 analyses: $5.37 per analysis +1,000 analyses: $0.87 per analysis +``` + +## 💰 Budget Planning + +### Conservative Budget (100 analyses/month) +``` +Business Plan: $499.95 +API Units: $37.40 +───────────────────────── +Monthly Total: $537.35 +Per Analysis: $5.37 +``` + +### Aggressive Budget (1,000 analyses/month) +``` +Business Plan: $499.95 +API Units: $374.00 +───────────────────────── +Monthly Total: $873.95 +Per Analysis: $0.87 +``` + +## 🔍 Monitoring Commands + +### In Your Code +```javascript +// Get detailed report +const report = getApiUnitReport() +console.log(report.summary) + +// Check specific API +console.log(report.breakdown.domain_domains) +``` + +### Console Output +Look for these log messages: +``` +[API UNITS] domain_domains: 7000 units (~$0.3500) +[API UNIT TRACKER] Session started +============================================================ +API UNIT USAGE REPORT +============================================================ +``` + +## 📋 Checklist Before Production + +- [ ] Business Plan subscription active ($499.95/month) +- [ ] API units purchased (recommend 400,000+ units) +- [ ] Display limit optimized (test with 100, use 1000 in prod) +- [ ] API usage tracking enabled (automatic in fetchUnifiedAnalysis) +- [ ] Monthly usage alerts configured +- [ ] Caching strategy implemented +- [ ] Error handling for "insufficient units" errors +- [ ] Budget approved for scale (see break-even analysis) + +## 🆘 Emergency Actions + +### Running Out of Units +1. Purchase more units immediately ($1 = 20,000 units) +2. Reduce display_limit temporarily +3. Cache results more aggressively +4. Pause non-critical analyses + +### Unexpected High Usage +1. Check getApiUnitReport() output +2. Look for phrase_this calls (100 units each!) +3. Verify display_limit settings +4. Review number of competitors per analysis + +## 📞 Quick Links + +- [Buy API Units](https://www.semrush.com/api-units/) +- [Check Balance](https://www.semrush.com/api-units/) +- [API Documentation](https://developer.semrush.com/) +- [Support](https://www.semrush.com/kb/5-api) + +--- + +**TL;DR**: +- Need Business Plan ($499.95/month) +- Each analysis costs ~$0.37 in API units +- Built-in tracking shows exact usage +- Avoid phrase_this for bulk operations +- 100+ analyses/month = cost-effective + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ecd5512 --- /dev/null +++ b/README.md @@ -0,0 +1,351 @@ +# 🚀 SEO Keyword Gap Analyzer + +A simple tool that helps you find keyword opportunities by comparing your website with your competitors. Think of it as a "spy tool" that shows you which keywords your competitors are ranking for, but you're not! + +--- + +## 🎯 What Does This Tool Do? + +Imagine you run a website and want to know: +- **What keywords are my competitors ranking for that I'm missing?** +- **How much traffic could I get from those keywords?** +- **Which keywords are easiest to rank for?** + +This tool answers all these questions automatically! It: + +1. ✅ Analyzes your website and up to 5 competitor websites +2. ✅ Finds "gap keywords" (keywords competitors rank for, but you don't) +3. ✅ Shows you important metrics like search volume, keyword difficulty, and more +4. ✅ Filters for "informational" keywords (educational content like guides, how-tos, etc.) +5. ✅ Displays domain metrics (authority score, traffic, backlinks) +6. ✅ Can send results directly to Hookpilot for content planning + +--- + +## 📋 What You Need Before Starting + +### 1. **A Computer** + - Works on Windows, Mac, or Linux + +### 2. **Internet Connection** + - You'll need internet to download the tool and fetch data from SEMrush + +### 3. **SEMrush API Key** 🔑 + - This is like a "password" that lets you access SEMrush data + - **How to get it:** + 1. Go to [SEMrush](https://www.semrush.com/) + 2. Sign up for an account (you'll need a paid Business plan) + 3. Go to your account settings + 4. Find "API" section and copy your API key + - **Cost:** SEMrush Business plan costs around 499.95 per month + +### 4. **Node.js Installed on Your Computer** + - Node.js is a program that lets you run this tool + - **How to check if you have it:** + - Open "Terminal" (Mac) or "Command Prompt" (Windows) + - Type: `node --version` + - If you see a version number (like v18.0.0), you're good! + - **If you don't have it:** + - Go to [nodejs.org](https://nodejs.org/) + - Download and install the "LTS" version (it's the recommended one) + +--- + +## 🛠️ How to Install and Run the Tool + +Follow these simple steps: + +### Step 1: Download the Project +1. Open "Terminal" (Mac) or "Command Prompt" (Windows) +2. Navigate to where you want to save the project (for example, your Documents folder) +3. If you have the project as a zip file, unzip it +4. If you received it from GitHub, type: + ``` + git clone [project-url] + ``` + +### Step 2: Open the Project Folder +1. In Terminal/Command Prompt, type: + ``` + cd semrush-vinita + ``` + (This moves you into the project folder) + +### Step 3: Install Required Components +1. Type this command and press Enter: + ``` + npm install + ``` +2. Wait for it to finish (it might take 2-3 minutes) +3. This downloads all the necessary "helpers" the tool needs to work + +### Step 4: Start the Tool +1. Type this command: + ``` + npm run dev + ``` +2. You should see a message like: + ``` + Local: http://localhost:5173/ + ``` +3. Open your web browser (Chrome, Firefox, Safari, etc.) +4. Go to: `http://localhost:5173/` +5. You should see the tool's homepage! 🎉 + +--- + +## 📖 How to Use the Tool + +### Step 1: Fill Out the Form + +You'll see a form with several fields: + +#### **Client Website URL** (Required) +- Type your website address here +- Example: `mywebsite.com` or `www.mywebsite.com` +- Don't include `http://` or `https://` + +#### **Competitor Websites** (At least 1 required) +- Type your competitors' website addresses +- You can add up to 5 competitors +- Example: `competitor1.com`, `competitor2.com` + +#### **Search Database** +- Choose your country/region +- Most people use `us` for United States +- Other options: `uk`, `ca`, `in`, etc. + +#### **Display Limit** +- How many keywords do you want to analyze? +- **Default: 10** (recommended for beginners) +- Higher numbers = more keywords but costs more API units +- Range: 1 to 10,000 + +#### **SEMrush API Key** 🔑 +- Paste your SEMrush API key here +- This is the "password" you got from SEMrush + +### Step 2: Click "Start Analysis" + +1. Click the big blue button +2. The tool will show a progress bar +3. Wait for the analysis to complete (usually 10-30 seconds) + +### Step 3: Review the Results + +The tool shows you several sections: + +#### **📊 API Usage Report** (Top) +- **Total Units:** How many SEMrush API units were used +- **Duration:** How long the analysis took +- This helps you track your SEMrush usage + +#### **📈 Domain Metrics** (Cards) +Shows information about each website analyzed: +- **Authority Score:** How "trustworthy" the website is (0-100, higher is better) +- **Organic Traffic:** How many visitors from Google +- **Organic Keywords:** How many keywords the site ranks for +- **Total Backlinks:** How many other websites link to this site +- **Referring Domains:** How many unique websites link to this site + +**💡 Tip:** Click on any domain card to see detailed information: +- Top 10 Organic Keywords +- Top 10 Referring Domains + +#### **🎯 Informational Gap Keywords** (Main Results) +This is the most important section! It shows keywords that: +- ✅ Your competitors rank for +- ❌ You don't rank for yet +- ✅ Are "informational" (educational content) + +For each keyword, you see: +- **Keyword:** The actual search term +- **Intent:** Should always be "Informational" +- **Search Volume:** How many people search for this per month (higher = more potential traffic) +- **Keyword Difficulty:** How hard it is to rank (0-100, lower = easier) +- **Competitors Found In:** Which competitors rank for this keyword + +#### **Actions You Can Take:** +1. **Sort:** Click column headers to sort by volume, difficulty, etc. +2. **Export:** Download results as CSV file for Excel/Google Sheets +3. **Send to Hookpilot:** Send the data to Hookpilot for content planning + +--- + +## 💰 Understanding API Costs + +SEMrush charges based on "API units." Here's what the tool uses: + +| Action | Cost per Item | +|--------|---------------| +| Keyword gap analysis | 80 units per keyword | +| Domain metrics | 10 units per domain | +| Keyword validation | 10 units per keyword | +| Backlink overview | 40 units per domain | +| Referring domains | 40 units per domain (×10) | +| Organic keywords | 10 units per domain (×10) | + +**Example Cost (with 10 keywords, 2 competitors):** +- Keyword gap: 10 × 80 = 800 units +- Keyword validation: 10 × 10 = 100 units +- Domain metrics: 3 × 10 = 30 units +- Backlinks: 3 × 40 = 120 units +- Referring domains: 3 × 10 × 40 = 1,200 units +- Organic keywords: 3 × 10 × 10 = 300 units +- **Total: ~2,550 units** (about 0.13 from your monthly SEMrush allowance) + +**💡 Tip:** Start with a small display limit (10-20) to test the tool before running large analyses. + +--- + +## ⚠️ Troubleshooting + +### Problem: "Cannot find module" error +**Solution:** +1. Make sure you ran `npm install` first +2. Try running it again + +### Problem: "Port already in use" error +**Solution:** +1. Another program is using the same port +2. Close the other program or change the port in `vite.config.js` + +### Problem: "ERROR 50 :: NOTHING FOUND" +**Possible causes:** +1. **Empty Client URL:** Make sure you entered your website address +2. **Invalid domain:** Check that website addresses are correct (no http://, no trailing slashes) +3. **No keywords found:** Try increasing the display limit +4. **Wrong API key:** Make sure your SEMrush API key is correct + +### Problem: "Network Error" or "Failed to fetch" +**Solution:** +1. Check your internet connection +2. Make sure your SEMrush API key is valid +3. Verify your SEMrush subscription is active + +### Problem: Tool is running slow +**Solution:** +1. Reduce the display limit (try 10-20 instead of 100+) +2. Analyze fewer competitors +3. Close other browser tabs + +--- + +## 🎓 Tips for Best Results + +### For Beginners: +1. **Start small:** Use display limit of 10-20 for your first analysis +2. **Test with 1-2 competitors:** Don't analyze 5 competitors right away +3. **Check your API usage:** The tool shows how many units you used + +### For Advanced Users: +1. **Higher display limits:** Use 50-100 for comprehensive analysis +2. **Multiple competitors:** Compare against 3-5 competitors for better insights +3. **Export data:** Download CSV files for deeper analysis in Excel + +### Interpreting Results: +- **High Volume + Low Difficulty = Great Opportunity!** 🎯 + - These keywords are searched a lot but easier to rank for + +- **Low Volume + High Difficulty = Skip for now** ⚠️ + - Not worth the effort for beginners + +- **Focus on Informational Keywords:** 📚 + - These are perfect for blog posts, guides, and tutorials + - Usually easier to rank than commercial keywords + +--- + +## 🤝 Need Help? + +If you're stuck or confused: + +1. **Check the Console Logs:** + - In your browser, press F12 (Windows) or Cmd+Option+I (Mac) + - Click on "Console" tab + - You'll see detailed logs of what's happening + +2. **Common Questions:** + - **Q: How do I stop the tool?** + - A: Press `Ctrl+C` in the Terminal/Command Prompt window + + - **Q: Can I run multiple analyses at once?** + - A: No, wait for one to finish before starting another + + - **Q: Where are my results saved?** + - A: Results are shown on screen. Use "Export CSV" to save them + + - **Q: How often can I use this?** + - A: As much as you want, but watch your SEMrush API unit balance + +--- + +## 📁 Project Structure + +``` +semrush-vinita/ +├── src/ +│ ├── components/ # Visual components (forms, results display) +│ ├── services/ # Logic for talking to SEMrush API +│ ├── App.jsx # Main application +│ └── main.jsx # Entry point +├── package.json # List of required components +├── vite.config.js # Configuration file +└── README.md # This file! +``` + +--- + +## 🔒 Privacy & Security + +- Your SEMrush API key is stored only in your browser (not on any server) +- All data analysis happens in your browser +- No data is collected or stored by this tool +- Only sent to Hookpilot if you click "Send to Hookpilot" button + +--- + +## 📝 Quick Start Checklist + +Before your first analysis, make sure you have: + +- [ ] Node.js installed on your computer +- [ ] Downloaded/cloned this project +- [ ] Ran `npm install` successfully +- [ ] Started the tool with `npm run dev` +- [ ] Opened `http://localhost:5173/` in your browser +- [ ] Have your SEMrush API key ready +- [ ] Know your website URL and competitor URLs + +--- + +## 🎉 You're All Set! + +Start analyzing keywords and finding content opportunities! Remember: + +1. **Start small** (10 keywords, 1-2 competitors) +2. **Watch your API usage** (shown in the results) +3. **Export valuable data** (CSV download button) +4. **Focus on high-volume, low-difficulty keywords** for quick wins + +Happy keyword hunting! 🔍✨ + +--- + +## 📞 Technical Support + +For SEMrush API issues: +- Visit: [SEMrush API Documentation](https://developer.semrush.com/api) +- Email: mail@semrush.com +- Phone: +1 (800) 815-9959 (12:00 PM - 5:00 PM EST/EDT, Monday - Friday) + +For tool-specific issues: +- Check the troubleshooting section above +- Review console logs in your browser (F12) +- Make sure all prerequisites are installed correctly + +--- + +**Version:** 1.0 +**Last Updated:** November 2025 +**Made with ❤️ for SEO professionals and content creators** diff --git a/SIMPLIFIED_FLOW.md b/SIMPLIFIED_FLOW.md new file mode 100644 index 0000000..2f164bb --- /dev/null +++ b/SIMPLIFIED_FLOW.md @@ -0,0 +1,201 @@ +# ✅ SIMPLIFIED FLOW - Informational Keywords Only + +## 🎯 What Changed + +### 1. **Fixed Domain Format** (Critical!) +**Before (WRONG):** +``` +*|or|competitor1|+|or|competitor2|-|or|client +``` + +**After (CORRECT per [SEMrush Tutorial](https://developer.semrush.com/api/basics/api-tutorials/analytics-api/#analyze-keyword-gaps-among-competitors/)):** +``` +-|or|client|+|or|competitor1|+|or|competitor2 +``` + +### 2. **Removed Unnecessary Complexity** +- ❌ Removed "At Least One Competitor" tab +- ❌ Removed "All Competitors Have" tab +- ✅ **Just show: Informational Gap Keywords (Intent=1)** + +### 3. **Added phrase_this Validation** +- ✅ Checkbox enabled by default +- ✅ Each keyword hits: `https://api.semrush.com/?type=phrase_this&phrase=KEYWORD&export_columns=In` +- ✅ Cost: 10 units per keyword +- ✅ Shows in Network tab + +### 4. **Simplified Output** +- ✅ Domain Metrics (kept) +- ✅ Informational Gap Keywords only +- ✅ API Usage Report with phrase_this breakdown +- ❌ No complex tabs or multiple views + +--- + +## 🔄 Complete Flow (Simplified) + +``` +USER FILLS FORM + ├─ Client: evendigit.com + ├─ Competitor 1: infidigit.com + ├─ Competitor 2: ignitevisibility.com + ├─ Display Limit: 100 + └─ ✅ Validate with phrase_this (checked) + ↓ +┌──────────────────────────────────────────────┐ +│ STEP 1: domain_domains API Call │ +└──────────────────────────────────────────────┘ +GET /api/semrush/?type=domain_domains + &domains=-|or|evendigit.com|+|or|infidigit.com|+|or|ignitevisibility.com + &display_limit=100 + &export_columns=Ph,P0,P1,P2,In,Nq,Kd,Co,Cp + +Response: 68 keywords (all intents) +Cost: 680 units + ↓ +┌──────────────────────────────────────────────┐ +│ STEP 2: phrase_this API Validation │ +│ (EACH keyword validated) │ +└──────────────────────────────────────────────┘ +For EACH of the 68 keywords: + +1. GET /api/semrush/?type=phrase_this&phrase=seo+tips&export_columns=In + Response: Intent=1 → ✅ KEEP + +2. GET /api/semrush/?type=phrase_this&phrase=buy+seo+tools&export_columns=In + Response: Intent=3 → ❌ REMOVE + +3. GET /api/semrush/?type=phrase_this&phrase=seo+guide&export_columns=In + Response: Intent=1 → ✅ KEEP + +... (65 more API calls) + +Total phrase_this calls: 68 +Cost: 68 × 10 = 680 units +Result: 46 keywords with Intent=1 + ↓ +┌──────────────────────────────────────────────┐ +│ STEP 3: Fetch Domain Metrics │ +└──────────────────────────────────────────────┘ +Parallel calls for 3 domains: + - domain_ranks (3 calls × 10 = 30 units) + - backlinks_overview (3 calls × 10 = 30 units) + - backlinks_refdomains (3 calls × 100 = 300 units) + +Cost: 360 units + ↓ +┌──────────────────────────────────────────────┐ +│ STEP 4: Display Results │ +└──────────────────────────────────────────────┘ +📊 API Usage Report + ├─ Total Units: 1,720 + ├─ Cost: $0.086 + └─ phrase_this: 68 calls, 680 units + +🎯 Informational Gap Keywords: 46 + ├─ Keywords competitors have, client doesn't + └─ All verified as Intent=1 (Informational) + +📍 Domain Metrics (3 domains) + ├─ Client: evendigit.com + │ ├─ Authority Score: 29 + │ ├─ Organic Traffic: 73 + │ └─ Organic Keywords: 641 + ├─ Competitor 1: infidigit.com + └─ Competitor 2: ignitevisibility.com +``` + +--- + +## 🧪 Console Output Example + +When you run with `validateWithPhrasethis=true`, you'll see: + +```bash +══════════════════════════════════════════════════════════ +🔍 PHRASE_THIS API VALIDATION - Intent Verification +══════════════════════════════════════════════════════════ +📊 Keywords to validate: 68 +💰 Cost: 68 keywords × 10 units = 680 units +💵 Estimated: $0.0340 +🔍 API: https://api.semrush.com/?type=phrase_this&phrase=KEYWORD&export_columns=In +══════════════════════════════════════════════════════════ + +[BATCH 1/7] Validating keywords 1-10... + 1. "seo tips" (Volume: 5400) + 2. "seo guide" (Volume: 3200) + 3. "content marketing" (Volume: 2100) + ... + +[API CALL] phrase_this for "seo tips" + URL: /api/semrush/?type=phrase_this&phrase=seo+tips&database=us&export_columns=In&key=*** + Cost: 10 units + Response: Intent = 1 +[RESULT] "seo tips" - Intent: Informational (1) ✅ INFORMATIONAL + + ✅ "seo tips" → Intent=1 (Informational) - KEPT + ❌ "buy seo tools" → Intent=3 (Transactional) - FILTERED OUT + ✅ "seo guide" → Intent=1 (Informational) - KEPT + +[BATCH 1] ✅ Complete: 7/10 keywords are Informational + +... (batches 2-7) + +══════════════════════════════════════════════════════════ +📊 VALIDATION COMPLETE +══════════════════════════════════════════════════════════ +📥 Keywords received: 68 +🔍 API calls made: 68 +✅ Keywords validated (Intent=1): 46 +❌ Keywords filtered out: 22 +💰 Total units consumed: 680 units +💵 Total cost: $0.0340 +══════════════════════════════════════════════════════════ +``` + +--- + +## 🌐 Network Tab - You'll See + +``` +Name Status Type Size +───────────────────────────────────────────────────────────────── +domain_domains?key=...&domains=-|or|... 200 xhr 2.1kB +phrase_this?phrase=seo+tips&... 200 xhr 15B +phrase_this?phrase=buy+seo+tools&... 200 xhr 15B +phrase_this?phrase=seo+guide&... 200 xhr 15B +phrase_this?phrase=content+marketing&... 200 xhr 15B +... (64 more phrase_this calls) +domain_ranks?domain=evendigit.com&... 200 xhr 156B +domain_ranks?domain=infidigit.com&... 200 xhr 156B +domain_ranks?domain=ignitevisibility.com&... 200 xhr 156B +backlinks_overview?target=evendigit.com&... 200 xhr 245B +... (more backlink calls) +``` + +--- + +## ✅ What You Get + +1. **API Usage Report** - Shows total units, cost, and phrase_this breakdown +2. **Informational Gap Keywords** - ONLY Intent=1 keywords (simplified!) +3. **Domain Metrics** - Authority Score, Traffic, Keywords, Backlinks +4. **No unnecessary tabs** - Clean, single view + +--- + +## 🚀 Ready to Test! + +1. **Refresh browser** (Cmd+Shift+R) +2. **Fill form:** + - Client: `evendigit.com` + - Competitor 1: `infidigit.com` + - Competitor 2: `ignitevisibility.com` + - Display Limit: `100` + - ✅ **Validate with phrase_this** (checked by default) +3. **Open Network tab** +4. **Click "Run Unified Analysis"** +5. **Watch phrase_this calls appear!** 🎉 + +Each keyword will hit the phrase_this API and you'll see it in the Network tab! + diff --git a/index.html b/index.html new file mode 100644 index 0000000..5259196 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + SEMrush Blog Keyword Research + + +
+ + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e5be68d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1909 @@ +{ + "name": "semrush-keyword-research", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "semrush-keyword-research", + "version": "1.0.0", + "dependencies": { + "axios": "^1.6.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.235", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", + "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fa21b5a --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "semrush-keyword-research", + "version": "1.0.0", + "description": "SEMrush Blog Keyword Research Tool", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "axios": "^1.6.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.0" + } +} + diff --git a/semrussh.postman_collection.json b/semrussh.postman_collection.json new file mode 100644 index 0000000..c1e1d4f --- /dev/null +++ b/semrussh.postman_collection.json @@ -0,0 +1,412 @@ +{ + "info": { + "_postman_id": "11ba170b-37d1-4619-b7e8-328ac6420564", + "name": "semrussh", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "_exporter_id": "39591404" + }, + "item": [ + { + "name": "phrase this", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://api.semrush.com/?type=phrase_this&key=61b5d6f509d57c3b7671d431ac5d7306&phrase=cloud security tips&database=us&export_columns=In", + "protocol": "https", + "host": [ + "api", + "semrush", + "com" + ], + "path": [ + "" + ], + "query": [ + { + "key": "type", + "value": "phrase_this" + }, + { + "key": "key", + "value": "61b5d6f509d57c3b7671d431ac5d7306" + }, + { + "key": "phrase", + "value": "cloud security tips" + }, + { + "key": "database", + "value": "us" + }, + { + "key": "export_columns", + "value": "In" + } + ] + } + }, + "response": [] + }, + { + "name": "competitor", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://api.semrush.com/?type=domain_domains&key=61b5d6f509d57c3b7671d431ac5d7306&database=us&domains=%2A%7Cor%7Cdigitalocean.com%7C%2B%7Cor%7Ccloud.google.com%7C%2D%7Cor%7Caws.amazon.com&display_sort=nq_desc&export_columns=Ph%2CP0%2CP1%2CP2%2CNq%2CKd%2CCo%2CCp&display_limit=100", + "protocol": "https", + "host": [ + "api", + "semrush", + "com" + ], + "path": [ + "" + ], + "query": [ + { + "key": "type", + "value": "domain_domains" + }, + { + "key": "key", + "value": "61b5d6f509d57c3b7671d431ac5d7306" + }, + { + "key": "database", + "value": "us" + }, + { + "key": "domains", + "value": "%2A%7Cor%7Cdigitalocean.com%7C%2B%7Cor%7Ccloud.google.com%7C%2D%7Cor%7Caws.amazon.com" + }, + { + "key": "display_sort", + "value": "nq_desc" + }, + { + "key": "export_columns", + "value": "Ph%2CP0%2CP1%2CP2%2CNq%2CKd%2CCo%2CCp" + }, + { + "key": "display_limit", + "value": "100" + } + ] + } + }, + "response": [] + }, + { + "name": "test", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "hapi/semrush/?type=domain_domains&key=61b5d6f509d57c3b7671d431ac5d7306&database=us&domains=*%7Cor%7Cdigitalocean.com%7Cor%7Ccloud.google.com%7C%2B%7Cor%7Caws.amazon.com%7C-%7Cor%7Ccloud.google.com&display_sort=nq_desc&export_columns=Ph,P0,P1,P2,Nq,Kd,Co,Cp&display_limit=100", + "host": [ + "hapi" + ], + "path": [ + "semrush", + "" + ], + "query": [ + { + "key": "type", + "value": "domain_domains" + }, + { + "key": "key", + "value": "61b5d6f509d57c3b7671d431ac5d7306" + }, + { + "key": "database", + "value": "us" + }, + { + "key": "domains", + "value": "*%7Cor%7Cdigitalocean.com%7Cor%7Ccloud.google.com%7C%2B%7Cor%7Caws.amazon.com%7C-%7Cor%7Ccloud.google.com" + }, + { + "key": "display_sort", + "value": "nq_desc" + }, + { + "key": "export_columns", + "value": "Ph,P0,P1,P2,Nq,Kd,Co,Cp" + }, + { + "key": "display_limit", + "value": "100" + } + ] + } + }, + "response": [] + }, + { + "name": "domain-rank", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://api.semrush.com/?type=domain_ranks&key=61b5d6f509d57c3b7671d431ac5d7306&domain=tech4bizsolutions.com&export_columns=Db,Dn,Rk,Or,Ot,Oc,Ad,At,Ac&database=in", + "protocol": "https", + "host": [ + "api", + "semrush", + "com" + ], + "path": [ + "" + ], + "query": [ + { + "key": "type", + "value": "domain_ranks" + }, + { + "key": "key", + "value": "61b5d6f509d57c3b7671d431ac5d7306" + }, + { + "key": "domain", + "value": "tech4bizsolutions.com" + }, + { + "key": "export_columns", + "value": "Db,Dn,Rk,Or,Ot,Oc,Ad,At,Ac" + }, + { + "key": "database", + "value": "in" + } + ] + } + }, + "response": [] + }, + { + "name": "organic-keyword", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://api.semrush.com/?type=domain_organic&key=61b5d6f509d57c3b7671d431ac5d7306&domain=tech4bizsolutions.com&database=us&export_columns=Ph,Po,Nq,Cp,Co,Kd&display_limit=100", + "protocol": "https", + "host": [ + "api", + "semrush", + "com" + ], + "path": [ + "" + ], + "query": [ + { + "key": "type", + "value": "domain_organic" + }, + { + "key": "key", + "value": "61b5d6f509d57c3b7671d431ac5d7306" + }, + { + "key": "domain", + "value": "tech4bizsolutions.com" + }, + { + "key": "database", + "value": "us" + }, + { + "key": "export_columns", + "value": "Ph,Po,Nq,Cp,Co,Kd" + }, + { + "key": "display_limit", + "value": "100" + } + ] + } + }, + "response": [] + }, + { + "name": "authority-backlink", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://api.semrush.com/analytics/v1/?key=61b5d6f509d57c3b7671d431ac5d7306&type=backlinks_overview&target=tech4bizsolutions.com&target_type=root_domain&export_columns=ascore,total,domains_num,urls_num,ips_num", + "protocol": "https", + "host": [ + "api", + "semrush", + "com" + ], + "path": [ + "analytics", + "v1", + "" + ], + "query": [ + { + "key": "key", + "value": "61b5d6f509d57c3b7671d431ac5d7306" + }, + { + "key": "type", + "value": "backlinks_overview" + }, + { + "key": "target", + "value": "tech4bizsolutions.com" + }, + { + "key": "target_type", + "value": "root_domain" + }, + { + "key": "export_columns", + "value": "ascore,total,domains_num,urls_num,ips_num" + } + ] + } + }, + "response": [] + }, + { + "name": "refer-domaIn", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://api.semrush.com/analytics/v1/?key=61b5d6f509d57c3b7671d431ac5d7306&type=backlinks_refdomains&target=tech4bizsolutions.com&target_type=root_domain&export_columns=domain_ascore,domain,backlinks_num", + "protocol": "https", + "host": [ + "api", + "semrush", + "com" + ], + "path": [ + "analytics", + "v1", + "" + ], + "query": [ + { + "key": "key", + "value": "61b5d6f509d57c3b7671d431ac5d7306" + }, + { + "key": "type", + "value": "backlinks_refdomains" + }, + { + "key": "target", + "value": "tech4bizsolutions.com" + }, + { + "key": "target_type", + "value": "root_domain" + }, + { + "key": "export_columns", + "value": "domain_ascore,domain,backlinks_num" + } + ] + } + }, + "response": [] + }, + { + "name": "backlink list", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://api.semrush.com/analytics/v1/?key=61b5d6f509d57c3b7671d431ac5d7306&type=backlinks&target=tech4bizsolutions.com&target_type=root_domain&export_columns=page_ascore,source_url,target_url,anchor&display_limit=200", + "protocol": "https", + "host": [ + "api", + "semrush", + "com" + ], + "path": [ + "analytics", + "v1", + "" + ], + "query": [ + { + "key": "key", + "value": "61b5d6f509d57c3b7671d431ac5d7306" + }, + { + "key": "type", + "value": "backlinks" + }, + { + "key": "target", + "value": "tech4bizsolutions.com" + }, + { + "key": "target_type", + "value": "root_domain" + }, + { + "key": "export_columns", + "value": "page_ascore,source_url,target_url,anchor" + }, + { + "key": "display_limit", + "value": "200" + } + ] + } + }, + "response": [] + }, + { + "name": "traffic-summary", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://api.semrush.com/analytics/ta/api/v3/toppages?key=61b5d6f509d57c3b7671d431ac5d7306&target=tech4bizsolutions.com&export_columns=page,traffic,traffic_share", + "protocol": "https", + "host": [ + "api", + "semrush", + "com" + ], + "path": [ + "analytics", + "ta", + "api", + "v3", + "toppages" + ], + "query": [ + { + "key": "key", + "value": "61b5d6f509d57c3b7671d431ac5d7306" + }, + { + "key": "target", + "value": "tech4bizsolutions.com" + }, + { + "key": "export_columns", + "value": "page,traffic,traffic_share" + } + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..cf20d70 --- /dev/null +++ b/src/App.css @@ -0,0 +1,181 @@ +.app { + min-height: 100vh; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + padding: 20px; +} + +.app-header { + text-align: center; + color: white; + margin-bottom: 40px; + animation: fadeInDown 0.6s ease-out; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 40px 20px; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3); +} + +.app-header h1 { + font-size: clamp(1.75rem, 5vw, 2.5rem); + font-weight: 700; + margin-bottom: 10px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); +} + +.app-header p { + font-size: clamp(0.9rem, 2.5vw, 1.1rem); + opacity: 0.95; + font-weight: 300; +} + +.mode-toggle { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin: 2rem 0; + max-width: 1000px; + margin-left: auto; + margin-right: auto; +} + +.mode-btn { + padding: 1rem 1.5rem; + border: 2px solid #e2e8f0; + background: white; + color: #4a5568; + border-radius: 12px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-height: 60px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.mode-btn:hover:not(:disabled) { + border-color: #667eea; + color: #667eea; + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(102, 126, 234, 0.15); +} + +.mode-btn.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-color: #667eea; + color: white; + box-shadow: 0 6px 12px rgba(102, 126, 234, 0.3); + transform: translateY(-2px); +} + +.mode-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.mode-btn.active:hover { + transform: translateY(-3px); + box-shadow: 0 8px 16px rgba(102, 126, 234, 0.4); +} + +.progress-container { + background: white; + padding: 30px; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + margin-bottom: 30px; + animation: fadeIn 0.4s ease-out; +} + +.progress-bar { + width: 100%; + height: 8px; + background: #e0e0e0; + border-radius: 4px; + overflow: hidden; + margin-bottom: 15px; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); + border-radius: 4px; + transition: width 0.3s ease; +} + +.progress-text { + text-align: center; + color: #555; + font-size: 0.95rem; + font-weight: 500; +} + +.error-message { + background: #fee; + color: #c33; + padding: 20px; + border-radius: 12px; + margin-bottom: 30px; + display: flex; + align-items: center; + gap: 12px; + box-shadow: 0 4px 12px rgba(204, 51, 51, 0.1); + animation: fadeIn 0.4s ease-out; +} + +.error-message svg { + flex-shrink: 0; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .app { + padding: 10px; + } + + .mode-toggle { + grid-template-columns: 1fr; + gap: 0.75rem; + margin: 1.5rem 0; + } + + .mode-btn { + padding: 0.875rem 1.25rem; + font-size: 0.9rem; + min-height: 50px; + } +} + +@media (max-width: 480px) { + .mode-btn { + padding: 0.75rem 1rem; + font-size: 0.85rem; + min-height: 45px; + } +} + diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..7df6351 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,98 @@ +import React, { useState } from 'react' +import './App.css' +import KeywordForm from './components/KeywordForm' +import UnifiedAnalysis from './components/UnifiedAnalysis' +import { + fetchUnifiedAnalysis +} from './services/semrushApi' + +function App() { + const [loading, setLoading] = useState(false) + const [unifiedAnalysis, setUnifiedAnalysis] = useState(null) + const [error, setError] = useState(null) + const [progress, setProgress] = useState({ current: 0, total: 0 }) + + const handleSearch = async (formData) => { + setLoading(true) + setError(null) + setUnifiedAnalysis(null) + + // Calculate total steps based on display limit + const numDomains = 1 + formData.competitors.filter(c => c && c.trim()).length + const displayLimit = formData.displayLimit || 10 + + // Progress steps: + // 1. domain_domains API call (1 step) + // 2. phrase_this validation (displayLimit steps) + // 3. Domain metrics (numDomains steps) + const totalSteps = 1 + displayLimit + numDomains + let currentStep = 0 + + setProgress({ current: 0, total: totalSteps }) + + try { + console.log('Starting unified analysis...') + + // Fetch unified analysis + setProgress({ current: 1, total: totalSteps }) // Starting domain_domains + const analysisData = await fetchUnifiedAnalysis(formData) + + setUnifiedAnalysis(analysisData) + setProgress({ current: totalSteps, total: totalSteps }) + + } catch (err) { + setError(err.message || 'An error occurred while fetching data') + } finally { + setLoading(false) + } + } + + return ( +
+
+

🚀 Unified SEO Analysis Tool

+

Comprehensive metrics + Keyword research + Industry detection + Hookpilot integration

+
+ + + + {loading && ( +
+
+
0 ? (progress.current / progress.total) * 100 : 0}%` + }} + /> +
+

+ {progress.total > 0 + ? `Analyzing ${progress.total} operations (Keywords validation + Domain metrics)...` + : 'Initializing analysis...'} +

+
+ )} + + {error && ( +
+ + + + {error} +
+ )} + + {unifiedAnalysis && ( + + )} +
+ ) +} + +export default App \ No newline at end of file diff --git a/src/components/ComprehensiveMetrics.css b/src/components/ComprehensiveMetrics.css new file mode 100644 index 0000000..150ae68 --- /dev/null +++ b/src/components/ComprehensiveMetrics.css @@ -0,0 +1,471 @@ +.comprehensive-metrics { + max-width: 1600px; + margin: 0 auto; + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; +} + +/* Header */ +.metrics-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 30px; + padding: 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + color: white; +} + +.header-content { + flex: 1; +} + +.header-content h2 { + margin: 0 0 8px 0; + font-size: 28px; + font-weight: 700; +} + +.header-content p { + margin: 0; + opacity: 0.9; + font-size: 16px; +} + +.hookpilot-status { + padding: 8px 16px; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + margin-left: 16px; +} + +.hookpilot-status.success { + background: rgba(76, 175, 80, 0.2); + color: #4caf50; + border: 1px solid rgba(76, 175, 80, 0.3); +} + +.hookpilot-status.error { + background: rgba(244, 67, 54, 0.2); + color: #f44336; + border: 1px solid rgba(244, 67, 54, 0.3); +} + +/* Table Container */ +.metrics-table-container { + background: white; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; + margin-bottom: 24px; +} + +.metrics-table { + width: 100%; + border-collapse: collapse; + background: white; +} + +.metrics-table th { + background: #f9fafb; + padding: 16px 12px; + text-align: left; + font-weight: 600; + font-size: 14px; + color: #374151; + border-bottom: 2px solid #e5e7eb; + white-space: nowrap; +} + +.metrics-table td { + padding: 16px 12px; + border-bottom: 1px solid #f3f4f6; + font-size: 14px; + vertical-align: top; +} + +.metrics-table tr:hover { + background: #f9fafb; +} + +.metrics-table tr.error-row { + background: #fef2f2; +} + +.metrics-table tr.error-row:hover { + background: #fee2e2; +} + +/* Cell Styles */ +.actions-cell { + width: 60px; + text-align: center; +} + +.expand-button { + background: #3b82f6; + color: white; + border: none; + border-radius: 6px; + padding: 8px 12px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s ease; +} + +.expand-button:hover { + background: #2563eb; + transform: scale(1.05); +} + +.domain-cell { + font-weight: 600; + color: #1f2937; + min-width: 200px; +} + +.client-badge { + margin-left: 8px; + padding: 2px 8px; + background: #3b82f6; + color: white; + border-radius: 12px; + font-size: 11px; + font-weight: 500; +} + +.metric-cell { + text-align: right; + font-weight: 500; + color: #1f2937; + min-width: 120px; +} + +.keywords-cell, .categories-cell { + max-width: 300px; + min-width: 250px; +} + +.stage-cell { + text-align: center; + min-width: 120px; +} + +.stage-badge { + padding: 6px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + color: white; + display: inline-flex; + align-items: center; + gap: 4px; +} + +/* Keywords List */ +.keywords-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.keyword-item { + display: flex; + flex-direction: column; + gap: 2px; + padding: 6px 8px; + background: #f3f4f6; + border-radius: 6px; + border-left: 3px solid #3b82f6; +} + +.keyword-phrase { + font-weight: 500; + color: #1f2937; + font-size: 13px; +} + +.keyword-metrics { + font-size: 11px; + color: #6b7280; +} + +.more-keywords { + font-size: 11px; + color: #6b7280; + font-style: italic; + text-align: center; + padding: 4px; +} + +/* Categories List */ +.categories-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.category-item { + display: flex; + flex-direction: column; + gap: 2px; + padding: 6px 8px; + background: #f0f9ff; + border-radius: 6px; + border-left: 3px solid #0ea5e9; +} + +.category-url { + font-weight: 500; + color: #1f2937; + font-size: 12px; + word-break: break-all; +} + +.category-traffic { + font-size: 11px; + color: #6b7280; +} + +.no-data { + color: #9ca3af; + font-style: italic; + font-size: 12px; +} + +/* Expanded Row */ +.expanded-row td { + padding: 0; + background: #f9fafb; +} + +.detailed-view { + padding: 24px; + background: white; + border-top: 1px solid #e5e7eb; +} + +.detailed-sections { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 24px; +} + +.section h4 { + margin: 0 0 16px 0; + font-size: 16px; + color: #1f2937; + border-bottom: 2px solid #e5e7eb; + padding-bottom: 8px; +} + +.metrics-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.metric-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: #f3f4f6; + border-radius: 6px; +} + +.metric-label { + font-size: 13px; + color: #6b7280; +} + +.metric-value { + font-size: 13px; + font-weight: 600; + color: #1f2937; +} + +/* Detailed Keywords */ +.keywords-detailed { + display: flex; + flex-direction: column; + gap: 8px; +} + +.keyword-detailed { + padding: 12px; + background: #f3f4f6; + border-radius: 8px; + border-left: 4px solid #3b82f6; +} + +.keyword-main { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; +} + +.keyword-rank { + background: #3b82f6; + color: white; + padding: 2px 6px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; +} + +.keyword-text { + font-weight: 500; + color: #1f2937; + font-size: 14px; +} + +.keyword-stats { + display: flex; + gap: 12px; + font-size: 12px; + color: #6b7280; +} + +/* Detailed Categories */ +.categories-detailed { + display: flex; + flex-direction: column; + gap: 8px; +} + +.category-detailed { + padding: 12px; + background: #f0f9ff; + border-radius: 8px; + border-left: 4px solid #0ea5e9; +} + +.category-url { + font-weight: 500; + color: #1f2937; + font-size: 13px; + margin-bottom: 6px; + word-break: break-all; +} + +.category-stats { + display: flex; + gap: 12px; + font-size: 12px; + color: #6b7280; +} + +/* Classification Legend */ +.classification-legend { + background: white; + border-radius: 12px; + padding: 24px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; +} + +.classification-legend h4 { + margin: 0 0 16px 0; + font-size: 18px; + color: #1f2937; +} + +.legend-items { + display: flex; + flex-direction: column; + gap: 12px; +} + +.legend-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background: #f9fafb; + border-radius: 8px; +} + +.legend-color { + width: 20px; + height: 20px; + border-radius: 50%; + flex-shrink: 0; +} + +.legend-item strong { + color: #1f2937; + font-weight: 600; +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .detailed-sections { + grid-template-columns: 1fr 1fr; + } + + .metrics-table th, + .metrics-table td { + padding: 12px 8px; + } +} + +@media (max-width: 768px) { + .comprehensive-metrics { + padding: 16px; + } + + .detailed-sections { + grid-template-columns: 1fr; + } + + .metrics-table { + font-size: 12px; + } + + .metrics-table th, + .metrics-table td { + padding: 8px 6px; + } + + .keywords-cell, .categories-cell { + max-width: 200px; + min-width: 150px; + } + + .keyword-item, .category-item { + padding: 4px 6px; + } + + .keyword-phrase, .category-url { + font-size: 11px; + } + + .keyword-metrics, .category-traffic { + font-size: 10px; + } +} + +@media (max-width: 480px) { + .metrics-table-container { + overflow-x: auto; + } + + .metrics-table { + min-width: 800px; + } + + .legend-items { + gap: 8px; + } + + .legend-item { + padding: 8px; + font-size: 14px; + } +} diff --git a/src/components/ComprehensiveMetrics.jsx b/src/components/ComprehensiveMetrics.jsx new file mode 100644 index 0000000..253991c --- /dev/null +++ b/src/components/ComprehensiveMetrics.jsx @@ -0,0 +1,261 @@ +import React, { useState } from 'react' +import './ComprehensiveMetrics.css' + +function ComprehensiveMetrics({ metricsData, hookpilotStatus }) { + const [expandedDomain, setExpandedDomain] = useState(null) + + if (!metricsData || metricsData.length === 0) { + return null + } + + const toggleDomainDetails = (domain) => { + setExpandedDomain(expandedDomain === domain ? null : domain) + } + + const getStageColor = (stage) => { + switch (stage) { + case 'NEW': return '#ff6b6b' + case 'GROWING': return '#ffa726' + case 'ESTABLISHED': return '#66bb6a' + case 'ERROR': return '#9e9e9e' + default: return '#9e9e9e' + } + } + + const getStageIcon = (stage) => { + switch (stage) { + case 'NEW': return '🌱' + case 'GROWING': return '📈' + case 'ESTABLISHED': return '🏆' + case 'ERROR': return '❌' + default: return '❓' + } + } + + const renderTopKeywords = (keywords) => { + if (!keywords || keywords.length === 0) { + return No keywords data + } + + return ( +
+ {keywords.slice(0, 5).map((keyword, index) => ( +
+ {keyword.phrase} + + SV: {keyword.searchVolume?.toLocaleString() || 'N/A'} | + KD: {keyword.keywordDifficulty || 'N/A'} | + Pos: {keyword.position || 'N/A'} + +
+ ))} + {keywords.length > 5 && ( +
+{keywords.length - 5} more keywords
+ )} +
+ ) + } + + const renderTopCategories = (categories) => { + if (!categories || categories.length === 0) { + return No categories data + } + + return ( +
+ {categories.map((category, index) => ( +
+ {category.url} + {category.traffic?.toLocaleString() || 0} visits +
+ ))} +
+ ) + } + + const renderDetailedView = (domainData) => { + if (!domainData || domainData.error) return null + + return ( +
+
+
+

📊 Complete Metrics

+
+
+ Authority Score: + {domainData.authorityScore} +
+
+ Organic Traffic: + {domainData.organicTraffic?.toLocaleString()} +
+
+ Organic Keywords: + {domainData.organicKeywords?.toLocaleString()} +
+
+ Paid Traffic: + {domainData.paidTraffic?.toLocaleString()} +
+
+ Paid Keywords: + {domainData.paidKeywords?.toLocaleString()} +
+
+ Total Backlinks: + {domainData.totalBacklinks?.toLocaleString()} +
+
+
+ +
+

🔍 Top 10 Keywords

+
+ {domainData.top10Keywords?.map((keyword, index) => ( +
+
+ #{index + 1} + {keyword.phrase} +
+
+ SV: {keyword.searchVolume?.toLocaleString() || 'N/A'} + KD: {keyword.keywordDifficulty || 'N/A'} + Pos: {keyword.position || 'N/A'} + CPC: ${keyword.cpc?.toFixed(2) || 'N/A'} +
+
+ ))} +
+
+ +
+

📄 Top Categories/Pages

+
+ {domainData.topCategories?.map((category, index) => ( +
+
{category.url}
+
+ Traffic: {category.traffic?.toLocaleString() || 0} + Keywords: {category.positionCount || 1} +
+
+ ))} +
+
+
+
+ ) + } + + return ( +
+
+
+

📊 Comprehensive Domain Metrics

+

Complete SEO analysis with all required metrics for SEO maturity assessment

+
+ {hookpilotStatus && ( +
+ {hookpilotStatus.success ? '✅ Sent to Hookpilot' : '❌ Hookpilot Error'} +
+ )} +
+ +
+ + + + + + + + + + + + + + + + {metricsData.map((domainData, index) => ( + + + + + + + + + + + + + {expandedDomain === domainData.domain && !domainData.error && ( + + + + )} + + ))} + +
ActionsDomainAuthority ScoreOrganic KeywordsTop 10 KeywordsReferring DomainsMonthly TrafficTop CategoriesStage
+ {!domainData.error && ( + + )} + + {domainData.domain} + {index === 0 && Client} + + {domainData.error ? 'Error' : domainData.authorityScore?.toLocaleString()} + + {domainData.error ? 'Error' : domainData.organicKeywords?.toLocaleString()} + + {domainData.error ? 'Error' : renderTopKeywords(domainData.top10Keywords)} + + {domainData.error ? 'Error' : domainData.referringDomains?.toLocaleString()} + + {domainData.error ? 'Error' : domainData.monthlyTraffic?.toLocaleString()} + + {domainData.error ? 'Error' : renderTopCategories(domainData.topCategories)} + + + {getStageIcon(domainData.stage)} {domainData.stage} + +
+ {renderDetailedView(domainData)} +
+
+ + {/* Classification Legend */} +
+

📋 SEO Maturity Classification Criteria

+
+
+ + NEW: AS < 20 OR Organic KW < 500 OR < 10 Ref Domains +
+
+ + GROWING: AS 20–40 OR Organic KW 500–3000 +
+
+ + ESTABLISHED: AS > 40 OR Organic KW > 3000 OR > 100 Ref Domains +
+
+
+
+ ) +} + +export default ComprehensiveMetrics diff --git a/src/components/KeywordForm.css b/src/components/KeywordForm.css new file mode 100644 index 0000000..4dbdc56 --- /dev/null +++ b/src/components/KeywordForm.css @@ -0,0 +1,169 @@ +.keyword-form { + margin-bottom: 30px; + animation: fadeIn 0.5s ease-out; +} + +.form-card { + background: white; + padding: clamp(20px, 4vw, 40px); + border-radius: 16px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.form-card:hover { + transform: translateY(-2px); + box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2); +} + +.form-section { + margin-bottom: 30px; +} + +.form-section:last-child { + margin-bottom: 0; +} + +.form-section h2 { + font-size: clamp(1.25rem, 3vw, 1.5rem); + color: #333; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 2px solid #667eea; +} + +.form-section h3 { + font-size: clamp(1rem, 2.5vw, 1.1rem); + color: #555; + margin-bottom: 15px; + font-weight: 600; +} + +.competitors-section { + margin-top: 20px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + color: #555; + font-weight: 500; + font-size: 0.95rem; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 12px 16px; + border: 2px solid #e0e0e0; + border-radius: 8px; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.form-group input:disabled, +.form-group select:disabled { + background: #f5f5f5; + cursor: not-allowed; + opacity: 0.6; +} + +.form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; +} + +.submit-btn { + width: 100%; + padding: 16px 24px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 10px; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin-top: 30px; +} + +.submit-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); +} + +.submit-btn:active:not(:disabled) { + transform: translateY(0); +} + +.submit-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.spinner { + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .form-card { + padding: 20px; + } + + .form-row { + grid-template-columns: 1fr; + } +} + +@media (max-width: 345px) { + .form-group input, + .form-group select { + font-size: 0.9rem; + padding: 10px 12px; + } + + .submit-btn { + font-size: 1rem; + padding: 14px 20px; + } +} + diff --git a/src/components/KeywordForm.jsx b/src/components/KeywordForm.jsx new file mode 100644 index 0000000..340b031 --- /dev/null +++ b/src/components/KeywordForm.jsx @@ -0,0 +1,212 @@ +import React, { useState } from 'react' +import './KeywordForm.css' + +const DATABASES = [ + { value: 'us', label: 'United States' }, + { value: 'uk', label: 'United Kingdom' }, + { value: 'ca', label: 'Canada' }, + { value: 'au', label: 'Australia' }, + { value: 'de', label: 'Germany' }, + { value: 'fr', label: 'France' }, + { value: 'es', label: 'Spain' }, + { value: 'it', label: 'Italy' }, + { value: 'br', label: 'Brazil' }, + { value: 'in', label: 'India' }, +] + +function KeywordForm({ onSearch, loading }) { + const [formData, setFormData] = useState({ + clientUrl: '', + competitor1: '', + competitor2: '', + competitor3: '', + competitor4: '', + database: 'us', + displayLimit: 10, + apiKey: '61b5d6f509d57c3b7671d431ac5d7306' + }) + + const handleSubmit = (e) => { + e.preventDefault() + + if (!formData.clientUrl.trim()) { + alert('Please enter your client website URL') + return + } + + const competitors = [ + formData.competitor1, + formData.competitor2, + formData.competitor3, + formData.competitor4 + ].filter(c => c.trim()) + + if (competitors.length === 0) { + alert('Please enter at least one competitor URL') + return + } + + onSearch({ + ...formData, + competitors + }) + } + + const handleChange = (e) => { + const { name, value } = e.target + setFormData(prev => ({ + ...prev, + [name]: value + })) + } + + return ( +
+
+
+

Website Information

+ +
+ + +
+ +
+

Competitor URLs (1-4)

+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+

Search Parameters

+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ + +
+
+ ) +} + +export default KeywordForm + diff --git a/src/components/KeywordResults.css b/src/components/KeywordResults.css new file mode 100644 index 0000000..ae6a189 --- /dev/null +++ b/src/components/KeywordResults.css @@ -0,0 +1,230 @@ +.keyword-results { + background: white; + padding: clamp(20px, 4vw, 40px); + border-radius: 16px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); + animation: slideUp 0.5s ease-out; +} + +.results-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + flex-wrap: wrap; + gap: 15px; +} + +.results-title h2 { + font-size: clamp(1.25rem, 3vw, 1.75rem); + color: #333; + margin-bottom: 5px; +} + +.results-count { + display: inline-block; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 4px 12px; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 600; + margin-left: 10px; +} + +.export-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + background: white; + color: #667eea; + border: 2px solid #667eea; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; +} + +.export-btn:hover { + background: #667eea; + color: white; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); +} + +.table-container { + overflow-x: auto; + border-radius: 12px; + border: 1px solid #e0e0e0; +} + +.keywords-table { + width: 100%; + border-collapse: collapse; + min-width: 600px; +} + +.keywords-table thead { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.keywords-table th { + padding: 16px 12px; + text-align: left; + font-weight: 600; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; + position: relative; +} + +.keywords-table th.sortable { + cursor: pointer; + user-select: none; + transition: background 0.2s ease; +} + +.keywords-table th.sortable:hover { + background: rgba(255, 255, 255, 0.1); +} + +.sort-indicator { + margin-left: 5px; + font-size: 1.1rem; +} + +.keywords-table tbody tr { + border-bottom: 1px solid #e0e0e0; + transition: background 0.2s ease; +} + +.keywords-table tbody tr:hover { + background: #f8f9ff; +} + +.keywords-table tbody tr:last-child { + border-bottom: none; +} + +.keywords-table td { + padding: 16px 12px; + color: #555; + font-size: 0.95rem; +} + +.keyword-phrase { + font-weight: 600; + color: #333; + max-width: 300px; + word-wrap: break-word; +} + +.keyword-volume { + font-weight: 500; + color: #667eea; +} + +.difficulty-cell { + display: flex; + align-items: center; +} + +.difficulty-badge { + padding: 4px 10px; + border-radius: 6px; + font-weight: 600; + font-size: 0.85rem; + text-align: center; + min-width: 50px; +} + +.difficulty-easy { + background: #d4edda; + color: #155724; +} + +.difficulty-medium { + background: #fff3cd; + color: #856404; +} + +.difficulty-hard { + background: #f8d7da; + color: #721c24; +} + +.difficulty-unknown { + background: #e2e3e5; + color: #383d41; +} + +.keyword-cpc { + font-weight: 500; +} + +.intent-badge { + display: inline-block; + background: #e3f2fd; + color: #1565c0; + padding: 4px 12px; + border-radius: 6px; + font-weight: 600; + font-size: 0.85rem; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .results-header { + flex-direction: column; + align-items: flex-start; + } + + .table-container { + border-radius: 8px; + } + + .keywords-table th, + .keywords-table td { + padding: 12px 8px; + font-size: 0.85rem; + } + + .keyword-phrase { + max-width: 200px; + } +} + +@media (max-width: 345px) { + .keywords-table { + min-width: 100%; + } + + .keywords-table th, + .keywords-table td { + padding: 10px 6px; + font-size: 0.8rem; + } + + .results-count { + font-size: 0.75rem; + padding: 3px 10px; + } + + .export-btn { + padding: 8px 16px; + font-size: 0.9rem; + } +} + diff --git a/src/components/KeywordResults.jsx b/src/components/KeywordResults.jsx new file mode 100644 index 0000000..7001599 --- /dev/null +++ b/src/components/KeywordResults.jsx @@ -0,0 +1,158 @@ +import React, { useState } from 'react' +import './KeywordResults.css' + +function KeywordResults({ keywords }) { + const [sortBy, setSortBy] = useState('volume') + const [sortOrder, setSortOrder] = useState('desc') + + const sortedKeywords = [...keywords].sort((a, b) => { + let aVal, bVal + + switch (sortBy) { + case 'volume': + aVal = parseInt(a.volume) || 0 + bVal = parseInt(b.volume) || 0 + break + case 'difficulty': + aVal = parseInt(a.difficulty) || 0 + bVal = parseInt(b.difficulty) || 0 + break + case 'cpc': + aVal = parseFloat(a.cpc) || 0 + bVal = parseFloat(b.cpc) || 0 + break + case 'phrase': + return sortOrder === 'asc' + ? a.phrase.localeCompare(b.phrase) + : b.phrase.localeCompare(a.phrase) + default: + aVal = 0 + bVal = 0 + } + + return sortOrder === 'asc' ? aVal - bVal : bVal - aVal + }) + + const handleSort = (field) => { + if (sortBy === field) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc') + } else { + setSortBy(field) + setSortOrder('desc') + } + } + + const exportToCSV = () => { + const headers = ['Keyword', 'Search Volume', 'Keyword Difficulty', 'CPC', 'Competition', 'Intent'] + const rows = keywords.map(kw => [ + kw.phrase, + kw.volume || 'N/A', + kw.difficulty || 'N/A', + kw.cpc || 'N/A', + kw.competition || 'N/A', + kw.intent || 'Informational' + ]) + + const csv = [ + headers.join(','), + ...rows.map(row => row.map(cell => `"${cell}"`).join(',')) + ].join('\n') + + const blob = new Blob([csv], { type: 'text/csv' }) + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `semrush-keywords-${new Date().toISOString().split('T')[0]}.csv` + a.click() + window.URL.revokeObjectURL(url) + } + + return ( +
+
+
+

Informational Keywords Found

+ {keywords.length} keywords +
+ +
+ +
+ + + + + + + + + + + + + {sortedKeywords.map((keyword, index) => ( + + + + + + + + + ))} + +
handleSort('phrase')} className="sortable"> + Keyword + {sortBy === 'phrase' && ( + {sortOrder === 'asc' ? '↑' : '↓'} + )} + handleSort('volume')} className="sortable"> + Search Volume + {sortBy === 'volume' && ( + {sortOrder === 'asc' ? '↑' : '↓'} + )} + handleSort('difficulty')} className="sortable"> + Difficulty + {sortBy === 'difficulty' && ( + {sortOrder === 'asc' ? '↑' : '↓'} + )} + handleSort('cpc')} className="sortable"> + CPC + {sortBy === 'cpc' && ( + {sortOrder === 'asc' ? '↑' : '↓'} + )} + CompetitionIntent
{keyword.phrase} + {keyword.volume ? parseInt(keyword.volume).toLocaleString() : 'N/A'} + +
+ + {keyword.difficulty || 'N/A'} + +
+
+ {keyword.cpc ? `${keyword.cpc}` : 'N/A'} + + {keyword.competition || 'N/A'} + + Informational +
+
+
+ ) +} + +function getDifficultyLevel(difficulty) { + const diff = parseInt(difficulty) + if (isNaN(diff)) return 'unknown' + if (diff < 30) return 'easy' + if (diff < 60) return 'medium' + return 'hard' +} + +export default KeywordResults + diff --git a/src/components/SEOMaturityAnalysis.css b/src/components/SEOMaturityAnalysis.css new file mode 100644 index 0000000..56f537b --- /dev/null +++ b/src/components/SEOMaturityAnalysis.css @@ -0,0 +1,881 @@ +.seo-maturity-analysis { + margin: 2rem 0; + padding: 2rem; + background: white; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +/* Metrics Cards */ +.metrics-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.metric-card { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 1.5rem; + border-radius: 12px; + display: flex; + align-items: center; + gap: 1rem; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.metric-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); +} + +.metric-card.authority { + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%); + box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3); +} + +.metric-card.keywords { + background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%); + box-shadow: 0 4px 12px rgba(78, 205, 196, 0.3); +} + +.metric-card.traffic { + background: linear-gradient(135deg, #45b7d1 0%, #96c93d 100%); + box-shadow: 0 4px 12px rgba(69, 183, 209, 0.3); +} + +.metric-card.backlinks { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + box-shadow: 0 4px 12px rgba(240, 147, 251, 0.3); +} + +.metric-icon { + font-size: 2.5rem; + opacity: 0.9; +} + +.metric-content { + flex: 1; +} + +.metric-value { + font-size: 2rem; + font-weight: 700; + line-height: 1; + margin-bottom: 0.25rem; +} + +.metric-label { + font-size: 0.9rem; + font-weight: 600; + opacity: 0.9; + margin-bottom: 0.25rem; +} + +.metric-subtitle { + font-size: 0.75rem; + opacity: 0.7; +} + +.metric-stage { + font-size: 0.8rem; + font-weight: 600; + background: rgba(255, 255, 255, 0.2); + padding: 0.25rem 0.5rem; + border-radius: 12px; + display: inline-block; + margin-top: 0.5rem; +} + +.analysis-header { + text-align: center; + margin-bottom: 2rem; +} + +.analysis-header h2 { + color: #2d3748; + margin-bottom: 0.5rem; + font-size: 1.8rem; +} + +.analysis-header p { + color: #718096; + font-size: 1rem; +} + +.summary-section { + background: #f7fafc; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; + border-left: 4px solid #667eea; +} + +.summary-section h3 { + color: #2d3748; + margin-bottom: 1rem; + font-size: 1.2rem; +} + +.summary-content { + color: #4a5568; + line-height: 1.6; + font-size: 1rem; +} + +.results-table-container { + overflow-x: auto; + margin-bottom: 2rem; +} + +.seo-maturity-table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border: 1px solid #e2e8f0; +} + +.seo-maturity-table th { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 1.25rem 1rem; + text-align: left; + font-weight: 600; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; + position: relative; +} + +.seo-maturity-table th:first-child { + border-top-left-radius: 12px; +} + +.seo-maturity-table th:last-child { + border-top-right-radius: 12px; +} + +.seo-maturity-table td { + padding: 1.25rem 1rem; + border-bottom: 1px solid #e2e8f0; + vertical-align: top; + transition: background-color 0.2s ease; +} + +.seo-maturity-table tr:hover { + background: linear-gradient(90deg, #f7fafc 0%, #edf2f7 100%); +} + +.seo-maturity-table tr.error-row { + background: linear-gradient(90deg, #fed7d7 0%, #feb2b2 100%); + color: #c53030; +} + +.seo-maturity-table tr:last-child td { + border-bottom: none; +} + +.domain-cell { + font-weight: 600; + min-width: 150px; +} + +.client-badge { + display: inline-block; + background: #667eea; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.7rem; + margin-left: 0.5rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.metric-cell { + text-align: right; + font-family: 'Courier New', monospace; + font-weight: 600; + color: #2d3748; +} + +.keywords-cell { + min-width: 200px; +} + +.top-keywords { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.keyword-tag { + background: #e2e8f0; + color: #4a5568; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + white-space: nowrap; +} + +.more-keywords { + color: #718096; + font-size: 0.8rem; + font-style: italic; +} + +.categories-cell { + min-width: 150px; +} + +.top-categories { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.category-item { + background: #f7fafc; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + color: #4a5568; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.more-categories { + color: #718096; + font-size: 0.8rem; + font-style: italic; +} + +.stage-cell { + text-align: center; +} + +.stage-badge { + display: inline-block; + padding: 0.5rem 1rem; + border-radius: 20px; + color: white; + font-weight: 600; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.classification-legend { + background: #f7fafc; + padding: 1.5rem; + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.classification-legend h4 { + color: #2d3748; + margin-bottom: 1rem; + font-size: 1.1rem; +} + +.legend-items { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.legend-item { + display: flex; + align-items: center; + gap: 0.75rem; + color: #4a5568; + font-size: 0.9rem; +} + +.legend-color { + width: 20px; + height: 20px; + border-radius: 4px; + flex-shrink: 0; +} + +/* Actions Cell */ +.actions-cell { + text-align: center; + width: 60px; +} + +.expand-button { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 0.5rem 0.75rem; + border-radius: 6px; + cursor: pointer; + font-size: 1rem; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3); +} + +.expand-button:hover { + transform: scale(1.1); + box-shadow: 0 4px 8px rgba(102, 126, 234, 0.5); +} + +/* Expanded Row */ +.expanded-row td { + padding: 2rem !important; + background: #f7fafc; + border-top: 2px solid #667eea; + border-bottom: 2px solid #667eea; +} + +/* Detailed Data Container */ +.detailed-data-container { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +/* Detailed Tabs */ +.detailed-tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 1.5rem; + border-bottom: 2px solid #e2e8f0; + padding-bottom: 0.5rem; +} + +.tab-button { + background: transparent; + border: none; + padding: 0.75rem 1.5rem; + font-size: 0.95rem; + font-weight: 600; + color: #718096; + cursor: pointer; + border-radius: 8px 8px 0 0; + transition: all 0.3s ease; + position: relative; +} + +.tab-button:hover { + background: #edf2f7; + color: #4a5568; +} + +.tab-button.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); +} + +.tab-content { + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Detailed Tables */ +.detailed-table { + width: 100%; + border-collapse: collapse; + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.detailed-table thead { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.detailed-table th { + padding: 1rem; + text-align: left; + font-weight: 600; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.detailed-table td { + padding: 0.875rem 1rem; + border-bottom: 1px solid #e2e8f0; + font-size: 0.9rem; + color: #4a5568; +} + +.detailed-table tbody tr:hover { + background: #f7fafc; + transition: background 0.2s ease; +} + +.detailed-table tbody tr:last-child td { + border-bottom: none; +} + +/* Keywords Table */ +.keyword-cell { + font-weight: 600; + color: #2d3748; + max-width: 300px; +} + +.position-cell { + text-align: center; + font-weight: 700; + color: #667eea; +} + +.volume-cell { + text-align: right; + font-family: 'Courier New', monospace; + color: #2d3748; +} + +.cpc-cell { + text-align: right; + font-family: 'Courier New', monospace; + color: #48bb78; + font-weight: 600; +} + +.competition-cell { + min-width: 120px; +} + +.progress-bar { + position: relative; + width: 100%; + height: 24px; + background: #e2e8f0; + border-radius: 12px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #48bb78 0%, #38a169 100%); + transition: width 0.3s ease; + border-radius: 12px; +} + +.progress-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 0.75rem; + font-weight: 600; + color: #2d3748; + z-index: 1; +} + +.difficulty-cell { + text-align: center; +} + +.difficulty-badge { + display: inline-block; + padding: 0.35rem 0.75rem; + border-radius: 16px; + font-weight: 700; + font-size: 0.85rem; + color: white; +} + +.difficulty-badge.difficulty-0 { + background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); +} + +.difficulty-badge.difficulty-1 { + background: linear-gradient(135deg, #ecc94b 0%, #d69e2e 100%); +} + +.difficulty-badge.difficulty-2 { + background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%); +} + +.difficulty-badge.difficulty-3, +.difficulty-badge.difficulty-4 { + background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%); +} + +/* Backlinks Summary */ +.backlinks-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; + padding: 1rem; + background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%); + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.summary-stat { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.stat-label { + font-size: 0.8rem; + color: #718096; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.stat-value { + font-size: 1.5rem; + font-weight: 700; + color: #2d3748; + font-family: 'Courier New', monospace; +} + +/* Backlinks Table */ +.ascore-cell { + text-align: center; +} + +.ascore-badge { + display: inline-block; + padding: 0.35rem 0.75rem; + border-radius: 16px; + font-weight: 700; + font-size: 0.85rem; + color: white; +} + +.ascore-badge.ascore-0 { + background: linear-gradient(135deg, #9e9e9e 0%, #757575 100%); +} + +.ascore-badge.ascore-1 { + background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%); +} + +.ascore-badge.ascore-2 { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.ascore-badge.ascore-3, +.ascore-badge.ascore-4 { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +} + +.url-cell { + max-width: 350px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.url-cell a { + color: #667eea; + text-decoration: none; + transition: color 0.2s ease; +} + +.url-cell a:hover { + color: #764ba2; + text-decoration: underline; +} + +.anchor-cell { + font-style: italic; + color: #718096; + max-width: 200px; +} + +/* Top Pages Table */ +.traffic-cell { + text-align: right; + font-family: 'Courier New', monospace; + font-weight: 600; + color: #48bb78; +} + +.count-cell { + text-align: center; + font-weight: 600; + color: #667eea; +} + +/* No Data State */ +.no-data { + text-align: center; + padding: 2rem !important; + color: #a0aec0; + font-style: italic; + font-size: 1rem; +} + +/* Scrollable Tables */ +.keywords-table-container, +.backlinks-table-container, +.pages-table-container { + max-height: 600px; + overflow-y: auto; + border-radius: 8px; +} + +/* Custom Scrollbar */ +.keywords-table-container::-webkit-scrollbar, +.backlinks-table-container::-webkit-scrollbar, +.pages-table-container::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.keywords-table-container::-webkit-scrollbar-track, +.backlinks-table-container::-webkit-scrollbar-track, +.pages-table-container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +.keywords-table-container::-webkit-scrollbar-thumb, +.backlinks-table-container::-webkit-scrollbar-thumb, +.pages-table-container::-webkit-scrollbar-thumb { + background: #667eea; + border-radius: 4px; +} + +.keywords-table-container::-webkit-scrollbar-thumb:hover, +.backlinks-table-container::-webkit-scrollbar-thumb:hover, +.pages-table-container::-webkit-scrollbar-thumb:hover { + background: #764ba2; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .seo-maturity-analysis { + padding: 1rem; + margin: 1rem 0; + } + + .metrics-cards { + grid-template-columns: 1fr; + gap: 1rem; + } + + .metric-card { + padding: 1rem; + } + + .metric-value { + font-size: 1.5rem; + } + + .metric-icon { + font-size: 2rem; + } + + .seo-maturity-table { + font-size: 0.8rem; + } + + .seo-maturity-table th, + .seo-maturity-table td { + padding: 0.75rem 0.5rem; + } + + .legend-items { + gap: 0.5rem; + } + + .legend-item { + font-size: 0.8rem; + } + + .detailed-tabs { + flex-direction: column; + gap: 0.5rem; + } + + .tab-button { + padding: 0.5rem 1rem; + font-size: 0.85rem; + } + + .detailed-data-container { + padding: 1rem; + } + + .detailed-table th, + .detailed-table td { + padding: 0.5rem; + font-size: 0.8rem; + } + + .backlinks-summary { + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + } + + .stat-value { + font-size: 1.2rem; + } + + .url-cell { + max-width: 200px; + } + + .keyword-cell { + max-width: 150px; + } + + .expanded-row td { + padding: 1rem !important; + } +} + +@media (max-width: 480px) { + .metrics-cards { + gap: 0.75rem; + } + + .metric-card { + padding: 0.75rem; + flex-direction: column; + text-align: center; + gap: 0.5rem; + } + + .metric-value { + font-size: 1.25rem; + } + + .metric-icon { + font-size: 1.5rem; + } + + .seo-maturity-table th, + .seo-maturity-table td { + padding: 0.5rem 0.25rem; + font-size: 0.7rem; + } + + .stage-badge { + padding: 0.25rem 0.5rem; + font-size: 0.7rem; + } + + .keyword-tag { + font-size: 0.7rem; + padding: 0.15rem 0.35rem; + } + + .category-item { + font-size: 0.7rem; + padding: 0.15rem 0.35rem; + } + + .detailed-tabs { + gap: 0.25rem; + } + + .tab-button { + padding: 0.5rem 0.75rem; + font-size: 0.75rem; + } + + .detailed-data-container { + padding: 0.75rem; + } + + .detailed-table th, + .detailed-table td { + padding: 0.5rem 0.25rem; + font-size: 0.7rem; + } + + .backlinks-summary { + grid-template-columns: 1fr; + gap: 0.5rem; + } + + .stat-value { + font-size: 1rem; + } + + .stat-label { + font-size: 0.7rem; + } + + .url-cell { + max-width: 150px; + } + + .keyword-cell { + max-width: 100px; + } + + .progress-bar { + height: 20px; + } + + .progress-text { + font-size: 0.65rem; + } + + .difficulty-badge, + .ascore-badge { + padding: 0.25rem 0.5rem; + font-size: 0.7rem; + } + + .expand-button { + padding: 0.35rem 0.5rem; + font-size: 0.85rem; + } + + .expanded-row td { + padding: 0.75rem !important; + } +} + +/* Extra responsive for 345px minimum */ +@media (max-width: 345px) { + .seo-maturity-analysis { + padding: 0.5rem; + } + + .detailed-data-container { + padding: 0.5rem; + } + + .tab-button { + padding: 0.4rem 0.5rem; + font-size: 0.7rem; + } + + .detailed-table th, + .detailed-table td { + padding: 0.4rem 0.2rem; + font-size: 0.65rem; + } + + .url-cell { + max-width: 100px; + } + + .keyword-cell { + max-width: 80px; + } +} diff --git a/src/components/SEOMaturityAnalysis.jsx b/src/components/SEOMaturityAnalysis.jsx new file mode 100644 index 0000000..e8e78b9 --- /dev/null +++ b/src/components/SEOMaturityAnalysis.jsx @@ -0,0 +1,406 @@ +import React, { useState } from 'react' +import './SEOMaturityAnalysis.css' + +function SEOMaturityAnalysis({ analysisResults }) { + const [expandedDomain, setExpandedDomain] = useState(null) + const [activeTab, setActiveTab] = useState('keywords') + + if (!analysisResults || analysisResults.length === 0) { + return null + } + + const clientData = analysisResults[0] // First domain is client + const competitorsData = analysisResults.slice(1) // Rest are competitors + + const generateSummary = () => { + if (!clientData || clientData.error) return 'Unable to generate summary due to data errors.' + + const clientStage = clientData.stage + const avgCompetitorAS = competitorsData + .filter(c => !c.error) + .reduce((sum, c) => sum + c.authorityScore, 0) / competitorsData.filter(c => !c.error).length || 0 + + const avgCompetitorKW = competitorsData + .filter(c => !c.error) + .reduce((sum, c) => sum + c.organicKeywords, 0) / competitorsData.filter(c => !c.error).length || 0 + + const avgCompetitorRD = competitorsData + .filter(c => !c.error) + .reduce((sum, c) => sum + c.referringDomains, 0) / competitorsData.filter(c => !c.error).length || 0 + + let summary = `Your domain (${clientData.domain}) is classified as ${clientStage} with an Authority Score of ${clientData.authorityScore}, ${clientData.organicKeywords.toLocaleString()} organic keywords, and ${clientData.referringDomains.toLocaleString()} referring domains. ` + + if (competitorsData.length > 0) { + summary += `Compared to your competitors (average: ${Math.round(avgCompetitorAS)} AS, ${Math.round(avgCompetitorKW).toLocaleString()} keywords, ${Math.round(avgCompetitorRD).toLocaleString()} referring domains), ` + + if (clientData.authorityScore > avgCompetitorAS) { + summary += `you have a stronger domain authority. ` + } else { + summary += `you have room to improve your domain authority. ` + } + + if (clientData.organicKeywords > avgCompetitorKW) { + summary += `You rank for more organic keywords than the average competitor. ` + } else { + summary += `You rank for fewer organic keywords than the average competitor. ` + } + + if (clientData.referringDomains > avgCompetitorRD) { + summary += `Your backlink profile is stronger with more referring domains.` + } else { + summary += `Your backlink profile could be strengthened with more referring domains.` + } + } + + return summary + } + + const getMetricCards = () => { + if (!clientData || clientData.error) return null + + return ( +
+
+
🏆
+
+
{clientData.authorityScore}
+
Authority Score
+
{clientData.stage}
+
+
+ +
+
🔍
+
+
{clientData.organicKeywords.toLocaleString()}
+
Organic Keywords
+
Ranking positions
+
+
+ +
+
📈
+
+
{clientData.organicTraffic.toLocaleString()}
+
Organic Traffic
+
Monthly visits
+
+
+ +
+
🔗
+
+
{clientData.referringDomains.toLocaleString()}
+
Referring Domains
+
Backlink sources
+
+
+
+ ) + } + + const getStageColor = (stage) => { + switch (stage) { + case 'NEW': return '#ff6b6b' + case 'GROWING': return '#ffa726' + case 'ESTABLISHED': return '#66bb6a' + case 'ERROR': return '#9e9e9e' + default: return '#9e9e9e' + } + } + + const getStageIcon = (stage) => { + switch (stage) { + case 'NEW': return '🌱' + case 'GROWING': return '📈' + case 'ESTABLISHED': return '🏆' + case 'ERROR': return '❌' + default: return '❓' + } + } + + const toggleDomainDetails = (domain) => { + setExpandedDomain(expandedDomain === domain ? null : domain) + } + + const renderDetailedData = (data) => { + if (!data || data.error) return null + + return ( +
+
+ + + +
+ +
+ {activeTab === 'keywords' && ( +
+ + + + + + + + + + + + + {data.topKeywords?.map((kw, idx) => ( + + + + + + + + + ))} + {(!data.topKeywords || data.topKeywords.length === 0) && ( + + + + )} + +
KeywordPositionSearch VolumeCPCCompetitionKeyword Difficulty
{kw.phrase}{kw.position}{kw.searchVolume?.toLocaleString()}${kw.cpc?.toFixed(2)} +
+
+ {(kw.competition * 100).toFixed(0)}% +
+
+ + {kw.keywordDifficulty} + +
No organic keywords data available
+
+ )} + + {activeTab === 'backlinks' && ( +
+
+
+ Total Backlinks: + {data.totalBacklinks?.toLocaleString()} +
+
+ Referring Domains: + {data.referringDomains?.toLocaleString()} +
+
+ Referring URLs: + {data.referringUrls?.toLocaleString()} +
+
+ Referring IPs: + {data.referringIps?.toLocaleString()} +
+
+ + + + + + + + + + + {data.backlinksList?.map((bl, idx) => ( + + + + + + + ))} + {(!data.backlinksList || data.backlinksList.length === 0) && ( + + + + )} + +
Page AuthoritySource URLTarget URLAnchor Text
+ + {bl.pageAscore} + + + + {bl.sourceUrl.length > 60 ? bl.sourceUrl.substring(0, 60) + '...' : bl.sourceUrl} + + + + {bl.targetUrl.length > 60 ? bl.targetUrl.substring(0, 60) + '...' : bl.targetUrl} + + {bl.anchor || '(no anchor)'}
No backlinks data available
+
+ )} + + {activeTab === 'pages' && ( +
+ + + + + + + + + + {data.topPages?.map((page, idx) => ( + + + + + + ))} + {(!data.topPages || data.topPages.length === 0) && ( + + + + )} + +
Page URLTrafficKeywords Count
+ + {page.url} + + {page.traffic?.toLocaleString()}{page.positionCount || 1}
No top pages data available
+
+ )} +
+
+ ) + } + + return ( +
+
+

SEO Maturity Analysis

+

Comprehensive domain-level metrics and classification

+
+ + {/* Key Metrics Cards */} + {getMetricCards()} + + {/* Summary Section */} +
+

Executive Summary

+
+ {generateSummary()} +
+
+ + {/* Results Table */} +
+ + + + + + + + + + + + + + + {analysisResults.map((result, index) => ( + + + + + + + + + + + + {expandedDomain === result.domain && !result.error && ( + + + + )} + + ))} + +
ActionsDomainAuthority ScoreOrganic KeywordsTotal BacklinksReferring DomainsMonthly TrafficStage
+ {!result.error && ( + + )} + + {result.domain} + {index === 0 && Client} + + {result.error ? 'Error' : result.authorityScore.toLocaleString()} + + {result.error ? 'Error' : result.organicKeywords.toLocaleString()} + + {result.error ? 'Error' : result.totalBacklinks.toLocaleString()} + + {result.error ? 'Error' : result.referringDomains.toLocaleString()} + + {result.error ? 'Error' : result.monthlyTraffic.toLocaleString()} + + + {getStageIcon(result.stage)} {result.stage} + +
+ {renderDetailedData(result)} +
+
+ + {/* Classification Legend */} +
+

Classification Criteria

+
+
+ + NEW: AS < 20 OR Organic KW < 500 OR < 10 Ref Domains +
+
+ + GROWING: AS 20–40 OR Organic KW 500–3000 +
+
+ + ESTABLISHED: AS > 40 OR Organic KW > 3000 OR > 100 Ref Domains +
+
+
+
+ ) +} + +export default SEOMaturityAnalysis diff --git a/src/components/UnifiedAnalysis.css b/src/components/UnifiedAnalysis.css new file mode 100644 index 0000000..a5fed4c --- /dev/null +++ b/src/components/UnifiedAnalysis.css @@ -0,0 +1,1435 @@ +.unified-analysis { + max-width: 1400px; + margin: 0 auto; + padding: 32px 24px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} + +/* Header */ +.analysis-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 32px; + padding: 32px; + background: white; + border-radius: 20px; + color: #1f2937; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); + border: 1px solid rgba(255, 255, 255, 0.8); +} + +.header-content h2 { + margin: 0 0 8px 0; + font-size: 32px; + font-weight: 800; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.header-content p { + margin: 0; + opacity: 0.7; + font-size: 16px; + color: #6b7280; +} + +.header-action-group { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 12px; +} + +.hookpilot-btn { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 24px; + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + border: none; + border-radius: 10px; + color: white; + font-weight: 600; + font-size: 15px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); + position: relative; + overflow: hidden; +} + +.hookpilot-btn::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.hookpilot-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4); + background: linear-gradient(135deg, #059669 0%, #047857 100%); +} + +.hookpilot-btn:hover:not(:disabled)::before { + width: 300px; + height: 300px; +} + +.hookpilot-btn:active:not(:disabled) { + transform: translateY(0); +} + +.hookpilot-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.hookpilot-btn.sending { + background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); +} + +.hookpilot-btn.success { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); + animation: successPulse 0.6s ease-out; +} + +@keyframes successPulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + } +} + +.button-spinner { + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.hookpilot-btn svg { + flex-shrink: 0; +} + +.export-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 8px; + color: white; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; +} + +.export-btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); +} + +.hookpilot-status { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + white-space: nowrap; +} + +.hookpilot-status.success { + background: rgba(76, 175, 80, 0.15); + color: #059669; + border: 1px solid rgba(76, 175, 80, 0.3); +} + +.hookpilot-status.error { + background: rgba(244, 67, 54, 0.15); + color: #dc2626; + border: 1px solid rgba(244, 67, 54, 0.3); +} + +.hookpilot-status svg { + flex-shrink: 0; +} + +/* Section Tabs */ +.section-tabs { + display: flex; + gap: 8px; + margin-bottom: 24px; + background: white; + padding: 8px; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; +} + +.tab-btn { + flex: 1; + padding: 12px 16px; + border: none; + background: transparent; + color: #6b7280; + font-weight: 500; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.tab-btn:hover { + background: #f3f4f6; + color: #374151; +} + +.tab-btn.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3); +} + +.tab-count { + font-size: 12px; + opacity: 0.8; + font-weight: 600; +} + +.competitor-count { + font-size: 14px; + color: #374151; + font-weight: 500; +} + +.difficulty-cell { + display: flex; + align-items: center; +} + +/* Overview Section */ +.overview-section { + display: flex; + flex-direction: column; + gap: 24px; +} + +/* Industry Card */ +.industry-card { + background: white; + border-radius: 12px; + padding: 24px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; +} + +.card-header h3 { + margin: 0 0 16px 0; + font-size: 20px; + color: #1f2937; +} + +.industry-content { + display: flex; + flex-direction: column; + gap: 12px; +} + +.industry-main { + display: flex; + align-items: center; + gap: 16px; +} + +.industry-name { + font-size: 24px; + font-weight: 700; + color: #3b82f6; +} + +.confidence-score { + padding: 6px 12px; + background: #f3f4f6; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + color: #6b7280; +} + +.supporting-terms { + font-size: 14px; + color: #6b7280; + line-height: 1.5; +} + +/* Quick Stats */ +.quick-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; +} + +.stat-card { + background: white; + border-radius: 12px; + padding: 20px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; + display: flex; + align-items: center; + gap: 16px; +} + +.stat-icon { + font-size: 32px; + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + color: white; +} + +.stat-content { + flex: 1; +} + +.stat-value { + font-size: 24px; + font-weight: 700; + color: #1f2937; + margin-bottom: 4px; +} + +.stat-label { + font-size: 14px; + color: #6b7280; + font-weight: 500; +} + +/* Rationale Section */ +.rationale-section { + background: white; + border-radius: 12px; + padding: 24px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; +} + +.rationale-section h3 { + margin: 0 0 16px 0; + font-size: 20px; + color: #1f2937; +} + +.rationale-content { + font-size: 16px; + line-height: 1.6; + color: #374151; + background: #f9fafb; + padding: 16px; + border-radius: 8px; + border-left: 4px solid #3b82f6; +} + +/* Metrics Section */ +.metrics-section { + background: white; + border-radius: 24px; + padding: 40px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); + border: 1px solid rgba(255, 255, 255, 0.8); + margin-bottom: 32px; +} + +.metrics-section h3 { + margin: 0 0 32px 0; + font-size: 28px; + font-weight: 800; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + display: flex; + align-items: center; + gap: 12px; +} + +.metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); + gap: 32px; +} + +.metric-card { + background: white; + border-radius: 20px; + padding: 0; + border: 1px solid #e5e7eb; + transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease; + overflow: hidden; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12); + cursor: pointer; + will-change: transform; + contain: layout style paint; +} + +.metric-card:hover { + box-shadow: 0 20px 60px rgba(102, 126, 234, 0.25); + transform: translateY(-6px); + border-color: rgba(102, 126, 234, 0.5); +} + +.metric-card:active { + transform: translateY(-3px); +} + +.metric-card.client-card { + background: linear-gradient(135deg, rgba(219, 234, 254, 0.95) 0%, rgba(191, 219, 254, 0.95) 100%); + border: 2px solid rgba(59, 130, 246, 0.3); +} + +.metric-card.client-card:hover { + border-color: rgba(59, 130, 246, 0.6); + box-shadow: 0 20px 60px rgba(59, 130, 246, 0.3); +} + +.metric-card.competitor-card { + background: rgba(255, 255, 255, 0.98); +} + +.metric-card.competitor-card:hover { + border-color: rgba(139, 92, 246, 0.5); + box-shadow: 0 20px 60px rgba(139, 92, 246, 0.2); +} + +/* Card Header */ +.metric-card .card-header { + padding: 24px 28px; + display: flex; + justify-content: space-between; + align-items: center; + background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%); + border-bottom: 2px solid rgba(102, 126, 234, 0.1); + transition: background-color 0.2s ease; +} + +.metric-card.client-card .card-header { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(37, 99, 235, 0.1) 100%); + border-bottom: 2px solid rgba(59, 130, 246, 0.2); +} + +.metric-card .card-header h4 { + margin: 0; + font-size: 20px; + font-weight: 700; + color: #1f2937; + display: flex; + align-items: center; + gap: 10px; +} + +.metric-card .card-header h4::before { + content: '🌐'; + font-size: 24px; +} + +/* Card Content */ +.metric-content { + padding: 28px; +} + +.metric-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.metric-header h4 { + margin: 0; + font-size: 18px; + font-weight: 700; + color: #1f2937; +} + +.client-badge, .competitor-badge { + padding: 8px 16px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.client-badge { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + color: white; +} + +.competitor-badge { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); + color: white; +} + +.metric-content { + display: flex; + flex-direction: column; + gap: 14px; +} + +.metric-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + background: #fafbfc; + border-radius: 12px; + border: 1px solid #e5e7eb; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.metric-item:hover { + border-color: rgba(102, 126, 234, 0.3); + background: white; +} + +.stage-item { + margin-top: 12px; + padding: 18px 20px; + background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); + border: 2px solid rgba(102, 126, 234, 0.2); +} + +.metric-values { + display: flex; + flex-direction: column; + gap: 12px; +} + +.metric-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 16px; + background: white; + border-radius: 8px; + border: 1px solid #f3f4f6; + transition: all 0.2s ease; +} + +.metric-row:hover { + border-color: #e5e7eb; + background: #fafbfc; +} + +.metric-label { + font-size: 13px; + color: #6b7280; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.metric-value { + font-size: 24px; + font-weight: 800; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stage-badge { + padding: 10px 18px; + border-radius: 25px; + font-size: 13px; + font-weight: 700; + color: white; + display: flex; + align-items: center; + gap: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Top Keywords and Referring Domains Sections */ +.top-keywords-section, +.referring-domains-section { + margin-top: 32px; + padding-top: 24px; + border-top: 2px solid #e5e7eb; +} + +.top-keywords-section h5, +.referring-domains-section h5 { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 600; + color: #374151; +} + +.keywords-list, +.domains-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 12px; +} + +.keyword-item, +.domain-item { + display: flex; + flex-direction: column; + gap: 6px; + padding: 14px 16px; + background: white; + border-radius: 8px; + border: 1px solid #f3f4f6; + transition: all 0.2s ease; +} + +.keyword-item:hover, +.domain-item:hover { + border-color: #e5e7eb; + background: #fafbfc; + transform: translateX(4px); +} + +.kw-phrase, +.domain-name { + font-size: 15px; + font-weight: 600; + color: #1f2937; + line-height: 1.4; +} + +.kw-details, +.domain-details { + font-size: 13px; + color: #6b7280; + font-weight: 500; +} + +/* Keywords Section */ +.keywords-section { + background: white; + border-radius: 24px; + padding: 40px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); + border: 1px solid rgba(255, 255, 255, 0.8); + margin-bottom: 32px; +} + +.keywords-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 32px; + flex-wrap: wrap; + gap: 20px; + padding-bottom: 20px; + border-bottom: 2px solid #e5e7eb; +} + +.keywords-header h3 { + margin: 0; + font-size: 26px; + font-weight: 800; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.keywords-controls { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} + +.filter-controls { + display: flex; + align-items: center; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #374151; + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: #3b82f6; +} + +.sort-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.sort-select { + padding: 8px 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + background: white; +} + +.sort-order-btn { + padding: 8px 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + background: white; + cursor: pointer; + font-size: 16px; + font-weight: bold; +} + +.keywords-stats { + display: flex; + gap: 20px; + margin-bottom: 24px; + flex-wrap: wrap; +} + +.stat-badge { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 20px; + background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%); + border-radius: 10px; + border: 1px solid #d1d5db; + transition: all 0.2s ease; +} + +.stat-badge:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.stat-badge .stat-label { + font-size: 14px; + color: #6b7280; + font-weight: 500; +} + +.stat-badge .stat-value { + font-size: 18px; + font-weight: 700; + color: #1f2937; +} + +.stat-label { + font-size: 12px; + color: #6b7280; + margin-bottom: 4px; +} + +.stat-value { + font-size: 18px; + font-weight: 700; + color: #1f2937; +} + +/* Table */ +.table-container { + overflow-x: auto; + border-radius: 8px; + border: 1px solid #e5e7eb; +} + +.keywords-table { + width: 100%; + border-collapse: collapse; + background: white; +} + +.keywords-table th { + background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%); + padding: 16px 20px; + text-align: left; + font-weight: 600; + font-size: 14px; + color: #374151; + border-bottom: 2px solid #e5e7eb; + white-space: nowrap; +} + +.keywords-table th.sortable { + cursor: pointer; + user-select: none; + transition: background-color 0.2s; +} + +.keywords-table th.sortable:hover { + background: #f3f4f6; +} + +.sort-indicator { + margin-left: 8px; + font-size: 12px; + color: #6b7280; +} + +.keywords-table td { + padding: 16px 20px; + border-bottom: 1px solid #f3f4f6; + font-size: 14px; + vertical-align: middle; +} + +.keywords-table tbody tr { + transition: background-color 0.2s ease; +} + +.keywords-table tbody tr:hover { + background-color: #f9fafb; +} + +.keyword-row.kept { + background: rgba(76, 175, 80, 0.05); +} + +.keyword-row.dropped { + background: rgba(244, 67, 54, 0.05); +} + +.keyword-phrase { + font-weight: 500; + color: #1f2937; +} + +.intent-badge { + padding: 4px 8px; + background: #e0f2fe; + color: #0277bd; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.keyword-volume { + font-weight: 500; + color: #1f2937; +} + +.difficulty-badge { + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.difficulty-easy { + background: #d1fae5; + color: #065f46; +} + +.difficulty-medium { + background: #fef3c7; + color: #92400e; +} + +.difficulty-hard { + background: #fee2e2; + color: #991b1b; +} + +.relevance-cell { + display: flex; + align-items: center; +} + +.relevance-badge { + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.relevance-high { + background: #d1fae5; + color: #065f46; +} + +.relevance-medium { + background: #fef3c7; + color: #92400e; +} + +.relevance-low { + background: #fee2e2; + color: #991b1b; +} + +.keyword-reason { + font-size: 12px; + color: #6b7280; + max-width: 200px; +} + +.status-badge { + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.status-badge.kept { + background: #d1fae5; + color: #065f46; +} + +.status-badge.dropped { + background: #fee2e2; + color: #991b1b; +} + +/* Industry Section */ +.industry-section { + background: white; + border-radius: 12px; + padding: 24px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; +} + +.industry-detailed h3 { + margin: 0 0 20px 0; + font-size: 20px; + color: #1f2937; +} + +.industry-info { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; +} + +.info-card { + background: #f9fafb; + border-radius: 8px; + padding: 20px; + border: 1px solid #e5e7eb; +} + +.info-card h4 { + margin: 0 0 12px 0; + font-size: 16px; + color: #1f2937; +} + +.industry-result { + display: flex; + flex-direction: column; + gap: 8px; +} + +.industry-name-large { + font-size: 24px; + font-weight: 700; + color: #3b82f6; +} + +.confidence-large { + font-size: 16px; + color: #6b7280; + font-weight: 500; +} + +.supporting-terms-list { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.term-tag { + padding: 6px 12px; + background: #e0f2fe; + color: #0277bd; + border-radius: 16px; + font-size: 12px; + font-weight: 500; +} + +.context-info p { + margin: 8px 0; + font-size: 14px; + color: #374151; +} + +/* Pagination */ +.pagination-container { + margin-top: 24px; + padding: 20px; + background: white; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + gap: 16px; +} + +.pagination-info { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 16px; + font-size: 14px; + color: #6b7280; +} + +.pagination-items-per-page { + display: flex; + align-items: center; + gap: 8px; +} + +.pagination-items-per-page label { + font-size: 14px; + color: #374151; + font-weight: 500; +} + +.items-per-page-select { + padding: 6px 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + background: white; + color: #374151; + cursor: pointer; + transition: all 0.2s ease; +} + +.items-per-page-select:hover { + border-color: #9ca3af; +} + +.items-per-page-select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.pagination-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + flex-wrap: wrap; +} + +.pagination-btn { + padding: 8px 16px; + border: 1px solid #d1d5db; + border-radius: 6px; + background: white; + color: #374151; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + min-width: 44px; +} + +.pagination-btn:hover:not(:disabled) { + background: #f3f4f6; + border-color: #9ca3af; + transform: translateY(-1px); +} + +.pagination-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + background: #f9fafb; +} + +.pagination-pages { + display: flex; + align-items: center; + gap: 4px; +} + +.pagination-page-btn { + padding: 8px 12px; + min-width: 40px; + border: 1px solid #d1d5db; + border-radius: 6px; + background: white; + color: #374151; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.pagination-page-btn:hover { + background: #f3f4f6; + border-color: #9ca3af; + transform: translateY(-1px); +} + +.pagination-page-btn.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-color: #667eea; + box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3); +} + +.pagination-page-btn.active:hover { + background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); + transform: translateY(-1px); +} + +.pagination-ellipsis { + padding: 8px 4px; + color: #9ca3af; + font-size: 14px; + font-weight: 500; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .unified-analysis { + padding: 16px; + } + + .analysis-header { + flex-direction: column; + gap: 16px; + align-items: stretch; + } + + .header-actions { + justify-content: center; + } + + .section-tabs { + flex-direction: column; + } + + .tab-btn { + justify-content: flex-start; + } + + .metrics-grid { + grid-template-columns: 1fr; + } + + .keywords-header { + flex-direction: column; + align-items: stretch; + } + + .keywords-controls { + justify-content: center; + } + + .keywords-stats { + justify-content: center; + } + + .keywords-table { + font-size: 12px; + } + + .keywords-table th, + .keywords-table td { + padding: 8px 12px; + } + + .industry-info { + grid-template-columns: 1fr; + } + + .pagination-container { + padding: 16px; + } + + .pagination-info { + flex-direction: column; + align-items: flex-start; + } + + .pagination-controls { + width: 100%; + justify-content: center; + } + + .pagination-pages { + flex-wrap: wrap; + justify-content: center; + } +} + +@media (max-width: 480px) { + .keywords-table th, + .keywords-table td { + padding: 6px 8px; + } + + .keyword-reason { + max-width: 150px; + font-size: 11px; + } + + .quick-stats { + grid-template-columns: 1fr; + } +} + +/* Modal Styles */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; + animation: fadeIn 0.2s ease-out; +} + +.modal-content { + background: white; + border-radius: 24px; + max-width: 950px; + width: 100%; + max-height: 85vh; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.4); + border: 1px solid rgba(255, 255, 255, 0.9); + animation: slideUp 0.25s cubic-bezier(0.4, 0, 0.2, 1); + will-change: transform, opacity; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 32px 40px; + border-bottom: 2px solid rgba(102, 126, 234, 0.15); + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.modal-header h3 { + margin: 0; + font-size: 26px; + font-weight: 800; +} + +.modal-close { + background: rgba(255, 255, 255, 0.25); + border: 2px solid rgba(255, 255, 255, 0.4); + color: white; + font-size: 24px; + width: 44px; + height: 44px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s ease, background 0.2s ease; + font-weight: 300; + line-height: 1; +} + +.modal-close:hover { + background: rgba(255, 255, 255, 0.35); + transform: rotate(90deg); +} + +.modal-body { + padding: 40px; + overflow-y: auto; + flex: 1; + background: #fafbfc; +} + +.modal-section { + margin-bottom: 40px; + background: white; + padding: 24px; + border-radius: 16px; + border: 1px solid #e5e7eb; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.modal-section:last-child { + margin-bottom: 0; +} + +.modal-section h4 { + margin: 0 0 20px 0; + display: flex; + align-items: center; + gap: 10px; + font-size: 20px; + font-weight: 700; + color: #1f2937; + padding-bottom: 12px; + border-bottom: 2px solid rgba(102, 126, 234, 0.15); +} + +.modal-table-wrapper { + overflow-x: auto; + border-radius: 12px; + border: 1px solid rgba(229, 231, 235, 0.6); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.modal-table { + width: 100%; + border-collapse: collapse; + background: white; +} + +.modal-table thead { + background: #f9fafb; +} + +.modal-table th { + padding: 16px 20px; + font-weight: 700; + font-size: 12px; + color: #4b5563; + text-transform: uppercase; + letter-spacing: 1px; + border-bottom: 2px solid rgba(102, 126, 234, 0.2); +} + +.modal-table td { + padding: 16px 20px; + border-bottom: 1px solid #f3f4f6; + color: #1f2937; + font-size: 15px; + font-weight: 500; +} + +.modal-table tbody tr { + transition: background-color 0.15s ease; +} + +.modal-table tbody tr:hover { + background: #f9fafb; +} + +.modal-table tbody tr:last-child td { + border-bottom: none; +} + +/* Card header hover effect */ +.metric-card .card-header:hover { + background: rgba(102, 126, 234, 0.12); +} + +.metric-card.client-card .card-header:hover { + background: rgba(59, 130, 246, 0.2); +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Responsive modal */ +@media (max-width: 768px) { + .modal-content { + max-width: 100%; + max-height: 95vh; + margin: 10px; + } + + .modal-header { + padding: 20px 24px; + } + + .modal-header h3 { + font-size: 20px; + } + + .modal-body { + padding: 24px; + } + + .modal-table th, + .modal-table td { + padding: 10px 12px; + font-size: 13px; + } +} \ No newline at end of file diff --git a/src/components/UnifiedAnalysis.jsx b/src/components/UnifiedAnalysis.jsx new file mode 100644 index 0000000..1bd7d02 --- /dev/null +++ b/src/components/UnifiedAnalysis.jsx @@ -0,0 +1,727 @@ +import React, { useState, useEffect, useMemo, useCallback } from 'react' +import './UnifiedAnalysis.css' +import { sendToHookpilot } from '../services/semrushApi' + +function UnifiedAnalysis({ analysisData }) { + const [hookpilotStatus, setHookpilotStatus] = useState(null) + const [sending, setSending] = useState(false) + const [sortBy, setSortBy] = useState('volume') + const [sortOrder, setSortOrder] = useState('desc') + const [currentPage, setCurrentPage] = useState(1) + const [itemsPerPage, setItemsPerPage] = useState(25) + const [selectedDomain, setSelectedDomain] = useState(null) // Track which domain modal is open + + if (!analysisData) return null + + const { + metrics, + gapKeywords, // Simplified - just gap keywords (Intent=1 only) + clientUrl, + competitorUrls, + clientStage, + apiUsage, + structuredResults + } = analysisData + + const clientData = metrics[0] + const competitorsData = metrics.slice(1) + + // Simplified - just show informational gap keywords + const informationalGapKeywords = gapKeywords || [] + const hasKeywordOpportunities = informationalGapKeywords.length > 0 + + const handleSendToHookpilot = async () => { + // Initial alert to inform user + alert('⏳ Sending data to Hookpilot...\n\nYou will be notified once the operation completes or if any issues occur.') + + setSending(true) + setHookpilotStatus(null) + + try { + const result = await sendToHookpilot(analysisData) + setHookpilotStatus(result) + + // Success alert + if (result.success) { + alert('✅ Success!\n\nData has been successfully sent to Hookpilot.') + } else { + alert('⚠️ Warning!\n\nData was sent but Hookpilot returned an unexpected response. Please check the status below.') + } + } catch (error) { + setHookpilotStatus({ success: false, error: error.message }) + + // Error alert + alert(`❌ Error!\n\nFailed to send data to Hookpilot:\n\n${error.message}`) + } finally { + setSending(false) + } + } + + // Use gap keywords directly (no tabs needed) + const currentKeywords = informationalGapKeywords + + // Memoize sorted keywords to prevent re-sorting on every render + const sortedKeywords = useMemo(() => { + return [...currentKeywords].sort((a, b) => { + let aVal, bVal + + switch (sortBy) { + case 'volume': + aVal = a.volume || 0 + bVal = b.volume || 0 + break + case 'difficulty': + aVal = a.kd || 0 + bVal = b.kd || 0 + break + case 'keyword': + return sortOrder === 'asc' + ? a.keyword.localeCompare(b.keyword) + : b.keyword.localeCompare(a.keyword) + default: + aVal = 0 + bVal = 0 + } + + return sortOrder === 'asc' ? aVal - bVal : bVal - aVal + }) + }, [currentKeywords, sortBy, sortOrder]) + + // Pagination calculations - memoized + const { totalPages, startIndex, endIndex, paginatedKeywords } = useMemo(() => { + const total = Math.ceil(sortedKeywords.length / itemsPerPage) + const start = (currentPage - 1) * itemsPerPage + const end = start + itemsPerPage + const paginated = sortedKeywords.slice(start, end) + + return { + totalPages: total, + startIndex: start, + endIndex: end, + paginatedKeywords: paginated + } + }, [sortedKeywords, itemsPerPage, currentPage]) + + // Reset to page 1 when sorting changes + useEffect(() => { + setCurrentPage(1) + }, [sortBy, sortOrder]) + + // Handle page change + const handlePageChange = useCallback((newPage) => { + if (newPage >= 1 && newPage <= totalPages) { + setCurrentPage(newPage) + // Scroll to keyword section instead of top of page + const keywordSection = document.querySelector('.keywords-section') + if (keywordSection) { + keywordSection.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + } + }, [totalPages]) + + // Handle items per page change + const handleItemsPerPageChange = useCallback((e) => { + const newItemsPerPage = parseInt(e.target.value) + setItemsPerPage(newItemsPerPage) + setCurrentPage(1) + }, []) + + // CSV Export functions + const exportToCSV = (data, filename, headers) => { + const csvHeaders = headers.join(',') + const csvRows = data.map(row => { + return headers.map(header => { + const value = row[header] || '' + // Escape quotes and wrap in quotes if contains comma or quote + if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) { + return `"${value.replace(/"/g, '""')}"` + } + return value + }).join(',') + }) + + const csv = [csvHeaders, ...csvRows].join('\n') + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }) + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + link.click() + window.URL.revokeObjectURL(url) + } + + const exportMissingKeywords = () => { + // Export informational gap keywords + const dataForExport = informationalGapKeywords.map(kw => ({ + ...kw, + competitor_domains: Array.isArray(kw.competitor_domains) + ? kw.competitor_domains.join(', ') + : kw.competitors_found_in || '' + })) + + exportToCSV( + dataForExport, + 'informational_gap_keywords.csv', + ['keyword', 'volume', 'kd', 'competitor_domains'] + ) + } + + const handleSort = useCallback((field) => { + if (sortBy === field) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc') + } else { + setSortBy(field) + setSortOrder('desc') + } + }, [sortBy, sortOrder]) + + const getStageColor = (stage) => { + switch (stage) { + case 'NEW': return '#ff6b6b' + case 'GROWING': return '#ffa726' + case 'ESTABLISHED': return '#66bb6a' + default: return '#9e9e9e' + } + } + + const getStageIcon = (stage) => { + switch (stage) { + case 'NEW': return '🌱' + case 'GROWING': return '📈' + case 'ESTABLISHED': return '🏆' + default: return '❓' + } + } + + const formatNumber = (num) => { + if (!num || num === 0) return '0' + return parseInt(num).toLocaleString() + } + + // Open domain details modal + const openDomainModal = useCallback((domainData) => { + setSelectedDomain(domainData) + }, []) + + // Close modal + const closeDomainModal = useCallback(() => { + setSelectedDomain(null) + }, []) + + return ( +
+ {/* Header */} +
+
+

🚀 SEO Analysis Results

+

Domain metrics, keyword gap analysis, and SEO maturity classification

+
+
+ + {hookpilotStatus && !hookpilotStatus.success && ( +
+ + + + {hookpilotStatus.error || 'Failed to send'} +
+ )} +
+
+ + {/* API Usage Report */} + {apiUsage && ( +
+

+ 📊 API Usage & Cost Report +

+
+
+
{apiUsage.totalUnits?.toLocaleString() || 0}
+
Total Units
+
+
+
{apiUsage.duration || '0s'}
+
Duration
+
+
+ {apiUsage.breakdown && apiUsage.breakdown.phrase_this && ( +
+ 🔍 phrase_this Validation: {apiUsage.breakdown.phrase_this.calls || 0} API calls, {apiUsage.breakdown.phrase_this.units?.toLocaleString() || 0} units consumed +
+ )} +
+ )} + + {/* Keyword Gap Analysis Section */} +
+
+

🎯 Informational Gap Keywords (Intent=1)

+
+ Sort by: + + +
+
+ + {/* Warning message if no keyword opportunities */} + {!hasKeywordOpportunities && ( +
+ + + + + No keyword opportunities found. All competitors may have similar keyword profiles to your client. + +
+ )} + + {/* Simplified Header - Just Informational Keywords */} + {hasKeywordOpportunities && ( +
+
+
+

+ 🎯 Informational Gap Keywords (Intent=1) +

+

+ Keywords competitors rank for, client doesn't - verified as Informational via phrase_this API +

+
+
+ {sortedKeywords.length} +
+
+
+ )} + + {hasKeywordOpportunities && ( + <> +
+ +
+ +
+ + + + + + + + + + + {paginatedKeywords.length === 0 ? ( + + + + ) : ( + paginatedKeywords.map((keyword, index) => ( + + + + + + + )) + )} + +
handleSort('keyword')} className="sortable"> + Keyword + {sortBy === 'keyword' && ( + {sortOrder === 'asc' ? '↑' : '↓'} + )} + handleSort('volume')} className="sortable"> + Volume + {sortBy === 'volume' && ( + {sortOrder === 'asc' ? '↑' : '↓'} + )} + handleSort('difficulty')} className="sortable"> + KD + {sortBy === 'difficulty' && ( + {sortOrder === 'asc' ? '↑' : '↓'} + )} + Competitors Found In
+

No keywords found in this category.

+
{keyword.keyword} + {keyword.volume ? formatNumber(keyword.volume) : 'N/A'} + +
+ + {keyword.kd || 'N/A'} + +
+
+ {keyword.competitors_found_in || (keyword.competitor_domains && keyword.competitor_domains.join(', ')) || 'N/A'} +
+
+ + {/* Pagination Controls */} + {hasKeywordOpportunities && sortedKeywords.length > 0 && ( +
+
+ + Showing {startIndex + 1} to {Math.min(endIndex, sortedKeywords.length)} of {sortedKeywords.length} keywords + +
+ + +
+
+ +
+ + + +
+ {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter(page => { + // Show first page, last page, current page, and pages around current + if (page === 1 || page === totalPages) return true + if (Math.abs(page - currentPage) <= 1) return true + return false + }) + .map((page, index, array) => { + const prevPage = array[index - 1] + const showEllipsis = prevPage && page - prevPage > 1 + + return ( + + {showEllipsis && ( + ... + )} + + + ) + })} +
+ + + +
+
+ )} + + )} +
+ + {/* Domain Metrics Section */} +
+

📊 Domain Metrics

+
+ {/* Client Metrics */} +
+
openDomainModal(clientData)} + style={{ cursor: 'pointer', userSelect: 'none' }} + > +

{clientData.domain}

+
+ Client + 👁️ +
+
+
+
+ Rank: + {formatNumber(clientData.rank)} +
+
+ Organic Traffic: + {formatNumber(clientData.organicTraffic)} +
+
+ Organic Keywords: + {formatNumber(clientData.organicKeywordsCount)} +
+
+ Authority Score: + {formatNumber(clientData.authorityScore)} +
+
+ Total Backlinks: + {formatNumber(clientData.totalBacklinks)} +
+
+ Referring Domains: + {formatNumber(clientData.referringDomainsCount)} +
+
+ SEO Stage: + + {getStageIcon(clientData.stage)} {clientData.stage} + +
+
+
+ + {/* Competitors Metrics */} + {competitorsData.map((competitor, index) => ( +
+
openDomainModal(competitor)} + style={{ cursor: 'pointer', userSelect: 'none' }} + > +

{competitor.domain}

+
+ Competitor {index + 1} + 👁️ +
+
+
+
+ Rank: + {formatNumber(competitor.rank)} +
+
+ Organic Traffic: + {formatNumber(competitor.organicTraffic)} +
+
+ Organic Keywords: + {formatNumber(competitor.organicKeywordsCount)} +
+
+ Authority Score: + {formatNumber(competitor.authorityScore)} +
+
+ Total Backlinks: + {formatNumber(competitor.totalBacklinks)} +
+
+ Referring Domains: + {formatNumber(competitor.referringDomainsCount)} +
+
+ SEO Stage: + + {getStageIcon(competitor.stage)} {competitor.stage} + +
+
+
+ ))} +
+
+ + {/* Domain Details Modal */} + {selectedDomain && ( +
+
e.stopPropagation()} + > +
+

📊 {selectedDomain.domain} - Details

+ +
+ +
+ {/* Top 10 Organic Keywords */} + {selectedDomain.top10Keywords && selectedDomain.top10Keywords.length > 0 && ( +
+

+ 📊 Top 10 Organic Keywords +

+
+ + + + + + + + + + {selectedDomain.top10Keywords.map((kw, idx) => ( + + + + + + ))} + +
KeywordSearch VolumeKD
{kw.phrase}{formatNumber(kw.searchVolume)}{kw.keywordDifficulty}
+
+
+ )} + + {/* Top 10 Referring Domains */} + {selectedDomain.top10ReferringDomains && selectedDomain.top10ReferringDomains.length > 0 && ( +
+

+ 🔗 Top 10 Referring Domains +

+
+ + + + + + + + + + {selectedDomain.top10ReferringDomains.map((ref, idx) => ( + + + + + + ))} + +
DomainAuthority ScoreBacklinks
{ref.domain}{ref.domainAscore}{formatNumber(ref.backlinksNum)}
+
+
+ )} +
+
+
+ )} +
+ ) +} + +function getDifficultyLevel(difficulty) { + const diff = parseInt(difficulty) + if (isNaN(diff)) return 'unknown' + if (diff < 30) return 'easy' + if (diff < 60) return 'medium' + return 'hard' +} + +export default UnifiedAnalysis diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..471d233 --- /dev/null +++ b/src/index.css @@ -0,0 +1,22 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +#root { + max-width: 1200px; + margin: 0 auto; +} + diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..46457ed --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,11 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) + diff --git a/src/services/semrushApi.js b/src/services/semrushApi.js new file mode 100644 index 0000000..1310195 --- /dev/null +++ b/src/services/semrushApi.js @@ -0,0 +1,1344 @@ +import axios from 'axios' + +const SEMRUSH_API_BASE = '/api/semrush/' +const SEMRUSH_ANALYTICS_API_BASE = '/api/analytics/v1/' + +/** + * SEMrush API Service - Comprehensive SEO Analysis & Keyword Gap Analysis + * + * KEY APIS USED: + * 1. domain_domains (Domain vs. Domain) - Efficient keyword gap analysis in single call + * - ⚠️ IMPORTANT: Intent filter does NOT work on this endpoint! + * - Supported filters: Ph, P0-P4, Nq, Cp, Co, Nr, Kd only (NOT In) + * - Intent filtering is done CLIENT-SIDE after fetching data + * - Compares client vs. all competitors simultaneously + * - Returns position data for all domains in one response + * - Cost: 10 units per line returned + * + * 2. phrase_this - Validate keyword intent for individual phrases + * - Returns Intent values: 0=Navigational, 1=Informational, 2=Commercial, 3=Transactional + * - Used for batch validation of informational keywords + * - Cost: 10 units per line (verified) + * + * 3. domain_ranks - Authority score, organic traffic, keyword counts + * - Cost: 10 units per domain + * + * 4. backlinks_overview - Backlink metrics and authority + * - Cost: 10 units per domain + * + * 5. backlinks_refdomains - Referring domain details + * - Cost: 10 units per line returned + * + * API UNIT PRICING: + * - Business Plan: $499.95/month (required) + * - API Units: $1 per 20,000 units + * - Units expire at end of month (no rollover) + * + * TYPICAL USAGE EXAMPLE (1 client + 3 competitors, 1000 keyword limit): + * - domain_domains: ~1,000 lines × 10 units = 10,000 units ($0.50) + * - domain_ranks: 4 domains × 10 units = 40 units ($0.002) + * - backlinks_overview: 4 domains × 10 units = 40 units ($0.002) + * - backlinks_refdomains: 40 lines × 10 units = 400 units ($0.02) + * - TOTAL: ~10,480 units ($0.52 per analysis) + * + * OPTIMIZATION: Uses domain_domains API for keyword comparison instead of multiple + * domain_organic calls, reducing API calls from N+1 to 1 (where N = number of competitors) + * SAVINGS: ~75% reduction in API units vs. separate domain_organic calls + * + * INTENT FILTERING: + * - ⚠️ Intent filter does NOT work on domain_domains endpoint (SEMrush API limitation) + * - Intent filtering only works on: domain_organic, phrase_related, phrase_these, phrase_this + * - We fetch Intent column (In) and filter client-side for Intent=1 (Informational) + * - This focuses on content opportunities rather than transactional/navigational keywords + * + * References: + * - https://developer.semrush.com/api/basics/api-tutorials/analytics-api/#analyze-keyword-gaps-among-competitors/ + * - https://api.semrush.com/?type=phrase_this&key={API_KEY}&phrase={PHRASE}&database=us&export_columns=In + * - https://www.semrush.com/kb/5-api (API Unit Costs) + */ + +// API Unit Cost Constants (units per request/line) +// ⚠️ WARNING: Some costs are ESTIMATED and need verification from actual API consumption +// Monitor your actual usage at: https://www.semrush.com/api-units/ +// +// VERIFIED costs (from official docs): +// - phrase_this (Keyword Overview): 10 units per line ✓ (https://developer.semrush.com/api/v3/analytics/keyword-reports/#batch-keyword-overview-one-database/) +// - Batch Keyword Overview: 10 units per line ✓ +// - Keyword Difficulty (phrase_kdi): 50 units per line ✓ +// - domain_organic: 10 units per line ✓ +// +// UNVERIFIED costs (need confirmation): +// - domain_domains, domain_ranks, backlinks_* - estimates based on similar endpoints +// +// Update these values based on your actual API consumption! +const API_UNIT_COSTS = { + domain_domains: 80, // ✓ VERIFIED - 80 units per line (Domain vs. Domain API) + domain_organic: 10, // ✓ VERIFIED - 10 units per line (Domain Organic Search Keywords) + domain_ranks: 10, // ✓ VERIFIED - 10 units per line (Domain Overview - one database) + backlinks_overview: 40, // ✓ VERIFIED - 40 units per request (Backlinks Overview) + backlinks_refdomains: 40, // ✓ VERIFIED - 40 units per line (Referring Domains) + phrase_this: 10, // ✓ VERIFIED - 10 units per line (Keyword Overview - one database) + phrase_kdi: 50, // ✓ VERIFIED - 50 units per line (Keyword Difficulty) + // Pricing + unitsPerDollar: 20000, // ✓ VERIFIED - $1 = 20,000 units + businessPlanCost: 499.95 // ✓ VERIFIED - Monthly subscription required +} + +// API Unit Usage Tracker +let apiUnitUsage = { + totalUnits: 0, + breakdown: {}, + startTime: null, + endTime: null +} + +// Initialize API unit tracking for a new session +export const initApiUnitTracking = () => { + apiUnitUsage = { + totalUnits: 0, + breakdown: {}, + startTime: Date.now(), + endTime: null + } + console.log('[API UNIT TRACKER] Session started') + console.log('[API UNIT TRACKER] ⚠️ WARNING: Some API unit costs are ESTIMATED') + console.log('[API UNIT TRACKER] Compare estimates with actual usage at: https://www.semrush.com/api-units/') +} + +// Track API unit consumption +const trackApiUnits = (apiType, unitsConsumed, details = '') => { + if (!apiUnitUsage.breakdown[apiType]) { + apiUnitUsage.breakdown[apiType] = { calls: 0, units: 0, details: [] } + } + + apiUnitUsage.breakdown[apiType].calls++ + apiUnitUsage.breakdown[apiType].units += unitsConsumed + apiUnitUsage.totalUnits += unitsConsumed + + if (details) { + apiUnitUsage.breakdown[apiType].details.push(details) + } + + const cost = (unitsConsumed / API_UNIT_COSTS.unitsPerDollar).toFixed(4) + console.log(`[API UNITS] ${apiType}: ${unitsConsumed} units (~$${cost}) - ${details}`) +} + +// Get API unit usage report +export const getApiUnitReport = () => { + apiUnitUsage.endTime = Date.now() + const duration = ((apiUnitUsage.endTime - apiUnitUsage.startTime) / 1000).toFixed(2) + const totalCost = (apiUnitUsage.totalUnits / API_UNIT_COSTS.unitsPerDollar).toFixed(4) + + const report = { + ...apiUnitUsage, + duration: `${duration}s`, + totalCost: `$${totalCost}`, + monthlyCost: API_UNIT_COSTS.businessPlanCost, + summary: { + totalUnits: apiUnitUsage.totalUnits, + totalCost: `$${totalCost}`, + businessPlanRequired: `$${API_UNIT_COSTS.businessPlanCost}/month`, + apiCallsMade: Object.values(apiUnitUsage.breakdown).reduce((sum, b) => sum + b.calls, 0) + } + } + + console.log('\n' + '='.repeat(60)) + console.log('API UNIT USAGE REPORT') + console.log('='.repeat(60)) + console.log(`Duration: ${duration}s`) + console.log(`Total API Calls: ${report.summary.apiCallsMade}`) + console.log(`Total Units Consumed: ${apiUnitUsage.totalUnits.toLocaleString()}`) + console.log(`Estimated Cost: $${totalCost}`) + console.log(`Monthly Business Plan: $${API_UNIT_COSTS.businessPlanCost}`) + console.log('\nBreakdown by API:') + + Object.entries(apiUnitUsage.breakdown).forEach(([apiType, data]) => { + const apiCost = (data.units / API_UNIT_COSTS.unitsPerDollar).toFixed(4) + console.log(` ${apiType}:`) + console.log(` - Calls: ${data.calls}`) + console.log(` - Units: ${data.units.toLocaleString()}`) + console.log(` - Cost: $${apiCost}`) + }) + + console.log('='.repeat(60) + '\n') + + return report +} + +// Calculate estimated units for a given configuration +// phrase_this validation is always enabled for all keywords +export const estimateApiUnits = (config) => { + const { clientUrl, competitors = [], displayLimit = 1000 } = config + const numCompetitors = competitors.length + const totalDomains = 1 + numCompetitors + + // Estimate lines returned (no Intent filter at API level, so expect full limit) + const estimatedLines = displayLimit + + const estimate = { + domain_domains: { + lines: estimatedLines, + units: estimatedLines * API_UNIT_COSTS.domain_domains, + cost: ((estimatedLines * API_UNIT_COSTS.domain_domains) / API_UNIT_COSTS.unitsPerDollar).toFixed(4) + }, + domain_ranks: { + domains: totalDomains, + units: totalDomains * API_UNIT_COSTS.domain_ranks, + cost: ((totalDomains * API_UNIT_COSTS.domain_ranks) / API_UNIT_COSTS.unitsPerDollar).toFixed(4) + }, + domain_organic: { + lines: totalDomains * 10, // 10 organic keywords per domain + units: totalDomains * 10 * API_UNIT_COSTS.domain_organic, + cost: ((totalDomains * 10 * API_UNIT_COSTS.domain_organic) / API_UNIT_COSTS.unitsPerDollar).toFixed(4) + }, + backlinks_overview: { + domains: totalDomains, + units: totalDomains * API_UNIT_COSTS.backlinks_overview, + cost: ((totalDomains * API_UNIT_COSTS.backlinks_overview) / API_UNIT_COSTS.unitsPerDollar).toFixed(4) + }, + backlinks_refdomains: { + lines: totalDomains * 10, // 10 referring domains per domain + units: totalDomains * 10 * API_UNIT_COSTS.backlinks_refdomains, + cost: ((totalDomains * 10 * API_UNIT_COSTS.backlinks_refdomains) / API_UNIT_COSTS.unitsPerDollar).toFixed(4) + }, + // phrase_this validation is ALWAYS enabled + phrase_this_validation: { + keywords: estimatedLines, + units: estimatedLines * API_UNIT_COSTS.phrase_this, + cost: ((estimatedLines * API_UNIT_COSTS.phrase_this) / API_UNIT_COSTS.unitsPerDollar).toFixed(4) + } + } + + const totalUnits = Object.values(estimate).reduce((sum, api) => sum + api.units, 0) + const totalCost = (totalUnits / API_UNIT_COSTS.unitsPerDollar).toFixed(2) + + estimate.total = { + units: totalUnits, + cost: `$${totalCost}`, + perAnalysis: `$${totalCost}`, + per100Analyses: `$${(totalCost * 100).toFixed(2)}` + } + + console.log('\n' + '='.repeat(60)) + console.log('ESTIMATED API UNIT CONSUMPTION') + console.log('='.repeat(60)) + console.log(`Configuration:`) + console.log(` - Client: ${clientUrl}`) + console.log(` - Competitors: ${numCompetitors}`) + console.log(` - Display Limit: ${displayLimit}`) + console.log(` - phrase_this Validation: ALWAYS ENABLED`) + console.log(`\nEstimated Usage:`) + Object.entries(estimate).forEach(([api, data]) => { + if (api !== 'total') { + const marker = api === 'phrase_this_validation' ? '🔍 ' : ' ' + console.log(`${marker}${api}: ${data.units.toLocaleString()} units (~$${data.cost})`) + } + }) + console.log(`\nTotal: ${totalUnits.toLocaleString()} units (~${estimate.total.cost})`) + console.log(`Cost per analysis: ${estimate.total.perAnalysis}`) + console.log(`Cost per 100 analyses: ${estimate.total.per100Analyses}`) + console.log(`\n🔍 phrase_this validation is always enabled for accurate Intent, Volume, and KD data`) + console.log('='.repeat(60) + '\n') + + return estimate +} + +// Parse SEMrush API response (semicolon-separated values) +// Format: Keyword;Intents;Search Volume;Keyword Difficulty +const parseSemrushResponse = (data) => { + const lines = data.trim().split('\n') + if (lines.length < 2) return [] + + // Parse headers - trim whitespace + const headers = lines[0].split(';').map(h => h.trim()) + console.log(`[PARSER] Headers:`, headers) + console.log(`[PARSER] Number of headers: ${headers.length}`) + + const results = [] + + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim() + if (!line) continue // Skip empty lines + + const values = line.split(';').map(v => v.trim()) + + // Create row object - map by header index position + // Expected order: Keyword (0), Intents (1), Search Volume (2), Keyword Difficulty (3) + const row = { + keyword: values[0] || '', // Column 0: Keyword + intent: values[1] || '0', // Column 1: Intents + volume: values[2] || '0', // Column 2: Search Volume + difficulty: values[3] || '0', // Column 3: Keyword Difficulty + // Also store with lowercase header names for compatibility + ph: values[0] || '', + in: values[1] || '0', + nq: values[2] || '0', + kd: values[3] || '0' + } + + // Also store by header name for flexibility + headers.forEach((header, index) => { + const headerLower = header.toLowerCase().replace(/\s+/g, '') + row[headerLower] = values[index] || '' + }) + + results.push(row) + } + + console.log(`[PARSER] Parsed ${results.length} rows`) + if (results.length > 0) { + console.log(`[PARSER] Sample row:`, results[0]) + } + + return results +} + +// SEO Maturity Classification Logic +// NEW: <20 AS or <500 organic KW +// GROWING: 20-40 AS or 500-3000 KW +// ESTABLISHED: >40 AS or >3000 KW +export const classifySEOMaturity = (authorityScore, organicKeywords) => { + if (authorityScore < 20 || organicKeywords < 500) { + return 'NEW' + } else if ((authorityScore >= 20 && authorityScore <= 40) || + (organicKeywords >= 500 && organicKeywords <= 3000)) { + return 'GROWING' + } else if (authorityScore > 40 || organicKeywords > 3000) { + return 'ESTABLISHED' + } + + return 'GROWING' // Default fallback +} + +// Phrase Intent API - Get intent for a specific keyword +// Uses phrase_this API to determine if a keyword is Informational (Intent=1) +// Also returns Search Volume (Nq) and Keyword Difficulty (Kd) +// Intent values: 0=Navigational, 1=Informational, 2=Commercial, 3=Transactional +export const fetchPhraseIntent = async (phrase, database, apiKey) => { + try { + const params = { + type: 'phrase_this', + key: apiKey, + phrase: phrase, + database: database, + export_columns: 'In,Nq,Kd' // Intent, Search Volume, Keyword Difficulty + } + + // Build full URL for logging (for transparency) + const queryString = new URLSearchParams(params).toString() + const fullUrl = `${SEMRUSH_API_BASE}?${queryString}` + + console.log(` → API: ${SEMRUSH_API_BASE}?type=phrase_this&phrase=${encodeURIComponent(phrase)}&database=${database}&export_columns=In,Nq,Kd&key=***`) + console.log(` → Cost: 10 units`) + + const response = await axios.get(SEMRUSH_API_BASE, { + params, + headers: { + 'Accept': 'text/plain' + } + }) + + if (response.data.includes('ERROR')) { + console.error(` ❌ ERROR: ${response.data}`) + return null + } + + const lines = response.data.trim().split('\n') + if (lines.length < 2) { + console.warn(` ⚠️ No data returned`) + return null + } + + // Parse response (format: "Intent;Search Volume;Keyword Difficulty\n1;1000;45") + const values = lines[1].split(';').map(v => v.trim()) + const intentValue = parseInt(values[0]) || 0 + const searchVolume = parseInt(values[1]) || 0 + const keywordDifficulty = parseFloat(values[2]) || 0 + + const intentMap = { + 0: 'Navigational', + 1: 'Informational', + 2: 'Commercial', + 3: 'Transactional' + } + + const result = { + phrase: phrase, + intent: intentValue, + intentLabel: intentMap[intentValue] || 'Unknown', + isInformational: intentValue === 1, + volume: searchVolume, + kd: keywordDifficulty + } + + // Track API units (10 units per keyword) + trackApiUnits('phrase_this', API_UNIT_COSTS.phrase_this, `Keyword: "${phrase}"`) + + console.log(` → Response: Intent=${result.intent} (${result.intentLabel}), Volume=${result.volume}, KD=${result.kd}`) + return result + + } catch (error) { + console.error(`[ERROR] Failed to fetch intent for "${phrase}":`, error.message) + return null + } +} + +// Domain vs. Domain API - Efficient keyword gap analysis +// Uses domain_domains API to compare client vs competitors in a single call +// All keywords are automatically validated using phrase_this API for accurate Intent, Volume, and KD +// CORRECT FORMAT: *|or|comp1.com|+|or|comp2.com|-|or|client.com +// Operators: * (start with first competitor), + (add competitor), - (exclude client to find gaps) +// Reference: https://developer.semrush.com/api/basics/api-tutorials/analytics-api/#analyze-keyword-gaps-among-competitors/ +export const fetchDomainVsDomain = async (clientUrl, competitors, database, apiKey, displayLimit = 1000) => { + const startTime = Date.now() + + try { + // Build domains parameter according to SEMrush official tutorial + // CORRECT FORMAT per SEMrush docs: + // -|or|mybrand.com = YOUR domain (keywords you're missing) + // +|or|competitor.com = Competitors (keywords they have) + // Reference: https://developer.semrush.com/api/basics/api-tutorials/analytics-api/#analyze-keyword-gaps-among-competitors/ + + // Validation: Check for empty client URL + if (!clientUrl || clientUrl.trim().length === 0) { + console.error('[ERROR] Client URL is empty or undefined!') + throw new Error('Client URL is required but was empty. Please fill in the Client Website URL field.') + } + + // Validation: Check for at least one competitor + if (!competitors || competitors.length === 0) { + console.error('[ERROR] No competitors provided!') + throw new Error('At least one competitor is required.') + } + + // Build domains parameter: START with client using -, then add competitors with + + // This format finds keywords that competitors have but client doesn't + let domainsParam = `-|or|${clientUrl.trim()}` // Start with client domain using - + + // Add all competitors with + + for (let i = 0; i < competitors.length; i++) { + if (competitors[i] && competitors[i].trim()) { + domainsParam += `|+|or|${competitors[i].trim()}` + } + } + + console.log('[DEBUG] Client URL:', clientUrl) + console.log('[DEBUG] Competitors:', competitors) + console.log('[DEBUG] Built domains parameter:', domainsParam) + console.log('[DEBUG] URL-encoded domains:', encodeURIComponent(domainsParam)) + console.log('[DEBUG] Position mapping: P0 = Client, P1-P' + competitors.length + ' = Competitors') + + // Build export_columns: Ph (Keyword), P0, P1, P2... (domain positions), In (Intent), Nq (volume), Kd, Co, Cp + // IMPORTANT: Position order matches domains parameter order! + // With domains=-|or|client|+|or|comp1|+|or|comp2: + // P0 = client position, P1 = comp1 position, P2 = comp2 position + let exportColumns = 'Ph,P0' // Keyword + Client position (P0) + for (let i = 0; i < competitors.length; i++) { + exportColumns += `,P${i + 1}` // Competitor positions (P1, P2, P3...) + } + exportColumns += ',In,Nq,Kd,Co,Cp' // Add Intent, Search Volume, KD, Competition, CPC + + // IMPORTANT: Intent filter (In) does NOT work on domain_domains endpoint! + // Per SEMrush API docs: domain_domains only supports Ph, P0-P4, Nq, Cp, Co, Nr, Kd filters + // Intent filtering ONLY works on: domain_organic, phrase_related, phrase_these, phrase_this + // Solution: We fetch all keywords and filter client-side for Intent=1 + + // Optional: Add volume filter (this DOES work on domain_domains) + let displayFilter + if (displayLimit >= 100) { + // For larger limits, filter by volume to get quality keywords + displayFilter = '+|Nq|Gt|100' // Volume > 100 (Intent filter will be done client-side) + console.log(`[INFO] Using volume filter: Nq > 100. Intent filtering will be done client-side.`) + } else { + // For small limits, no filter - get all keywords + displayFilter = undefined + console.log(`[INFO] No API filter applied. Intent filtering will be done client-side.`) + } + + const params = { + type: 'domain_domains', + key: apiKey, + database: database, + domains: domainsParam, + display_sort: 'nq_desc', // Sort by search volume descending + export_columns: exportColumns, + display_limit: displayLimit + } + + // Only add display_filter if defined + if (displayFilter) { + params.display_filter = displayFilter + } + + console.log(`\n[API CALL START] domain_domains (Domain vs. Domain)`) + console.log(`[REQUEST] URL: ${SEMRUSH_API_BASE}`) + console.log(`[REQUEST] Parameters:`, params) + console.log(`[REQUEST] Domains parameter (decoded): ${domainsParam}`) + console.log(`[REQUEST] Timestamp: ${new Date().toISOString()}`) + + const response = await axios.get(SEMRUSH_API_BASE, { + params, + headers: { + 'Accept': 'text/plain' + } + }) + + const endTime = Date.now() + const duration = endTime - startTime + + console.log(`[RESPONSE] Status: ${response.status} ${response.statusText}`) + console.log(`[RESPONSE] Duration: ${duration}ms`) + console.log(`[RESPONSE] Data length: ${response.data?.length || 0} characters`) + console.log(`[RESPONSE] Data preview (first 500 chars):`, response.data?.substring(0, 500)) + + if (response.data.includes('ERROR')) { + console.error(`[ERROR] API returned error in response:`, response.data) + throw new Error(response.data) + } + + // Parse the response + const lines = response.data.trim().split('\n') + if (lines.length < 2) { + console.warn(`[WARNING] No keyword gap data found`) + return { + clientKeywords: [], + competitorKeywordsList: competitors.map(() => []), + missingKeywordsAllCompetitors: [], + competitorKeywordsAtLeastOne: [], + allKeywords: [] + } + } + + // Parse headers + const headers = lines[0].split(';').map(h => h.trim()) + console.log(`[PARSER] Headers:`, headers) + + // Parse data rows + const allKeywords = [] + const clientKeywords = [] + const competitorKeywordsList = competitors.map(() => []) + const missingKeywordsAllCompetitors = [] + const competitorKeywordsAtLeastOne = [] + + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim() + if (!line) continue + + const values = line.split(';').map(v => v.trim()) + + // Map values to columns + // Format: Ph, P0, P1, P2, ..., In, Nq, Kd, Co, Cp + // IMPORTANT: With domains=-|or|client|+|or|comp1|+|or|comp2: + // P0 = client position, P1 = comp1, P2 = comp2 + const keyword = values[0] || '' + + // Client position is FIRST (P0) + const clientPosition = parseInt(values[1]) || 0 // P0 - 0 means not ranking + + // Competitor positions are AFTER client (P1, P2, P3...) + const competitorPositions = [] + for (let j = 0; j < competitors.length; j++) { + competitorPositions.push(parseInt(values[j + 2]) || 0) // P1, P2, P3... + } + + // After all positions: Intent, Volume, KD, Competition, CPC + const intentIndex = competitors.length + 2 + const intent = parseInt(values[intentIndex]) || 1 // In - Intent (1 = Informational) + const volume = parseInt(values[intentIndex + 1]) || 0 // Nq + const kd = parseFloat(values[intentIndex + 2]) || 0 // Kd + const competition = parseFloat(values[intentIndex + 3]) || 0 // Co + const cpc = parseFloat(values[intentIndex + 4]) || 0 // Cp + + const keywordData = { + keyword: keyword, + intent: intent, // 1 = Informational (filtered by API) + volume: volume, + kd: kd, + competition: competition, + cpc: cpc, + clientPosition: clientPosition, + competitorPositions: competitorPositions, + clientRanks: clientPosition > 0, + competitorsRanking: competitorPositions.filter(p => p > 0).length + } + + allKeywords.push(keywordData) + + // Add to client keywords if client ranks + if (clientPosition > 0) { + clientKeywords.push({ + keyword: keyword, + intent: intent, // Use actual intent from API + volume: volume, + kd: kd + }) + } + + // Add to competitor keyword lists + competitorPositions.forEach((position, compIndex) => { + if (position > 0) { + competitorKeywordsList[compIndex].push({ + keyword: keyword, + intent: intent, // Use actual intent from API + volume: volume, + kd: kd + }) + } + }) + + // Gap analysis: Keywords where client doesn't rank but at least one competitor does + if (clientPosition === 0) { + const competitorsWithKeyword = [] + competitorPositions.forEach((position, compIndex) => { + if (position > 0) { + competitorsWithKeyword.push(competitors[compIndex]) + } + }) + + // If at least one competitor ranks for this keyword, it's a gap opportunity + if (competitorsWithKeyword.length > 0) { + const gapKeyword = { + keyword: keyword, + volume: volume, + kd: kd, + intent: intent, // Use actual intent from API (filtered to 1 = Informational) + competitor_domains: competitorsWithKeyword, + competitors_found_in: competitorsWithKeyword.join(', ') + } + + // Add to gap keywords (at least one competitor has it) + missingKeywordsAllCompetitors.push(gapKeyword) + + // Keep for compatibility + competitorKeywordsAtLeastOne.push(gapKeyword) + } + } + } + + // Track API units consumed + const linesReturned = allKeywords.length + const unitsConsumed = linesReturned * API_UNIT_COSTS.domain_domains + trackApiUnits('domain_domains', unitsConsumed, `${linesReturned} lines returned`) + + console.log(`\n[DOMAIN_DOMAINS RESULTS - BEFORE INTENT FILTERING]`) + console.log(`Total keywords returned by API: ${allKeywords.length}`) + + // PHRASE_THIS API VALIDATION - ALWAYS ENABLED + // All keywords are validated using phrase_this API for accurate Intent, Volume, and KD + console.log(`\n══════════════════════════════════════════════════════════`) + console.log(`🔍 PHRASE_THIS API VALIDATION - Intent Verification`) + console.log(`══════════════════════════════════════════════════════════`) + console.log(`📊 Total keywords to validate: ${allKeywords.length}`) + console.log(`💰 Cost: ${allKeywords.length} × 10 units = ${allKeywords.length * 10} units`) + console.log(`💵 Est. cost: $${((allKeywords.length * 10) / 20000).toFixed(4)}`) + console.log(`🌐 API: https://api.semrush.com/?type=phrase_this&phrase=KEYWORD&export_columns=In,Nq,Kd`) + console.log(`══════════════════════════════════════════════════════════\n`) + + const validatedKeywords = [] + const batchSize = 10 + let totalCalls = 0 + + for (let i = 0; i < allKeywords.length; i += batchSize) { + const batch = allKeywords.slice(i, Math.min(i + batchSize, allKeywords.length)) + const batchNum = Math.floor(i / batchSize) + 1 + const totalBatches = Math.ceil(allKeywords.length / batchSize) + + console.log(`\n[BATCH ${batchNum}/${totalBatches}] Validating keywords ${i + 1}-${Math.min(i + batchSize, allKeywords.length)}:`) + + const validations = await Promise.all( + batch.map(async (kw, idx) => { + console.log(` ${i + idx + 1}. Calling phrase_this for "${kw.keyword}"...`) + const result = await fetchPhraseIntent(kw.keyword, database, apiKey) + totalCalls++ + return result + }) + ) + + let keptCount = 0 + validations.forEach((validation, idx) => { + const keyword = batch[idx] + if (validation?.isInformational) { + // Update with phrase_this API data (more accurate) + keyword.intent = validation.intent + keyword.intentLabel = validation.intentLabel + keyword.volume = validation.volume + keyword.kd = validation.kd + validatedKeywords.push(keyword) + keptCount++ + console.log(` ✅ "${keyword.keyword}" → Intent=${validation.intent} (Informational), Vol=${validation.volume}, KD=${validation.kd} - KEPT`) + } else if (validation) { + console.log(` ❌ "${keyword.keyword}" → Intent=${validation.intent} (${validation.intentLabel}), Vol=${validation.volume}, KD=${validation.kd} - REMOVED`) + } else { + console.log(` ⚠️ "${keyword.keyword}" → API failed - REMOVED`) + } + }) + + console.log(`[BATCH ${batchNum}] Result: ${keptCount}/${batch.length} keywords are Informational`) + } + + console.log(`\n══════════════════════════════════════════════════════════`) + console.log(`📊 VALIDATION COMPLETE`) + console.log(`══════════════════════════════════════════════════════════`) + console.log(`📥 Keywords received from domain_domains: ${allKeywords.length}`) + console.log(`🔍 phrase_this API calls made: ${totalCalls}`) + console.log(`✅ Informational keywords (Intent=1): ${validatedKeywords.length}`) + console.log(`❌ Filtered out: ${allKeywords.length - validatedKeywords.length}`) + console.log(`💰 Units consumed: ${totalCalls * 10} units`) + console.log(`💵 Cost: $${((totalCalls * 10) / 20000).toFixed(4)}`) + console.log(`══════════════════════════════════════════════════════════\n`) + + // Replace with validated results + allKeywords.length = 0 + allKeywords.push(...validatedKeywords) + + // Now filter all the derivative lists based on filtered allKeywords + // Create a Map of validated keywords (keyword name -> full keyword object with updated volume/KD) + const validatedKeywordMap = new Map( + allKeywords.map(kw => [kw.keyword.toLowerCase(), kw]) + ) + + // Filter client keywords and update with validated data + const filteredClientKeywords = clientKeywords + .filter(kw => validatedKeywordMap.has(kw.keyword.toLowerCase())) + .map(kw => { + const validated = validatedKeywordMap.get(kw.keyword.toLowerCase()) + return { ...kw, volume: validated.volume, kd: validated.kd, intent: validated.intent } + }) + + // Filter competitor keywords and update with validated data + const filteredCompetitorKeywordsList = competitorKeywordsList.map(compKws => + compKws + .filter(kw => validatedKeywordMap.has(kw.keyword.toLowerCase())) + .map(kw => { + const validated = validatedKeywordMap.get(kw.keyword.toLowerCase()) + return { ...kw, volume: validated.volume, kd: validated.kd, intent: validated.intent } + }) + ) + + // Filter gap keywords and update with validated data (THIS IS KEY!) + const filteredMissingAllCompetitors = missingKeywordsAllCompetitors + .filter(kw => validatedKeywordMap.has(kw.keyword.toLowerCase())) + .map(kw => { + const validated = validatedKeywordMap.get(kw.keyword.toLowerCase()) + return { ...kw, volume: validated.volume, kd: validated.kd, intent: validated.intent } + }) + + const filteredCompetitorAtLeastOne = competitorKeywordsAtLeastOne + .filter(kw => validatedKeywordMap.has(kw.keyword.toLowerCase())) + .map(kw => { + const validated = validatedKeywordMap.get(kw.keyword.toLowerCase()) + return { ...kw, volume: validated.volume, kd: validated.kd, intent: validated.intent } + }) + + // Replace with filtered results + clientKeywords.length = 0 + clientKeywords.push(...filteredClientKeywords) + + competitorKeywordsList.forEach((list, idx) => { + list.length = 0 + list.push(...filteredCompetitorKeywordsList[idx]) + }) + + missingKeywordsAllCompetitors.length = 0 + missingKeywordsAllCompetitors.push(...filteredMissingAllCompetitors) + + competitorKeywordsAtLeastOne.length = 0 + competitorKeywordsAtLeastOne.push(...filteredCompetitorAtLeastOne) + + console.log(`\n══════════════════════════════════════════════════════════`) + console.log(`📊 FINAL RESULTS - Informational Keywords (Intent=1)`) + console.log(`══════════════════════════════════════════════════════════`) + console.log(`✅ Total informational keywords: ${allKeywords.length}`) + console.log(`📍 Client (${clientUrl}): ${clientKeywords.length} keywords`) + competitors.forEach((comp, idx) => { + console.log(`📍 Competitor ${idx + 1} (${comp}): ${competitorKeywordsList[idx].length} keywords`) + }) + console.log(`🎯 Gap keywords (competitors have, client doesn't): ${missingKeywordsAllCompetitors.length}`) + console.log(`⏱️ Duration: ${duration}ms`) + console.log(`══════════════════════════════════════════════════════════\n`) + + return { + clientKeywords, + competitorKeywordsList, + gapKeywords: missingKeywordsAllCompetitors, // Simplified - just gap keywords + allKeywords + } + + } catch (error) { + const endTime = Date.now() + const duration = endTime - startTime + console.error(`[ERROR] domain_domains failed after ${duration}ms`) + console.error(`[ERROR] Error message:`, error.message) + console.error(`[ERROR] Error stack:`, error.stack) + if (error.response) { + console.error(`[ERROR] Response status:`, error.response.status) + console.error(`[ERROR] Response data:`, error.response.data) + } + console.log(`[API CALL END] domain_domains - Failed\n`) + throw error + } +} + +// Unified analysis combining comprehensive metrics and keyword research +// Uses domain_domains API (Domain vs. Domain) for efficient keyword gap analysis +// All keywords are automatically validated with phrase_this API +export const fetchUnifiedAnalysis = async (formData) => { + const { clientUrl, competitors, database, apiKey, displayLimit } = formData + const allDomains = [clientUrl, ...competitors] + + // Initialize API unit tracking + initApiUnitTracking() + + // Show cost estimate before starting + console.log('\n📊 PRE-ANALYSIS API UNIT ESTIMATE') + const estimate = estimateApiUnits({ clientUrl, competitors, displayLimit }) + + try { + // Step 1: Fetch keyword gap data using Domain vs. Domain API (SINGLE API CALL) + console.log('\n========================================') + console.log('[DOMAIN_DOMAINS API - KEYWORD GAP ANALYSIS]') + console.log('========================================') + console.log(`[CONFIG] Display limit: ${displayLimit}`) + console.log(`[CONFIG] Client: ${clientUrl}`) + console.log(`[CONFIG] Competitors: ${competitors.join(', ')}`) + + // Use Domain vs. Domain API for efficient keyword gap analysis (with phrase_this validation) + const keywordGapData = await fetchDomainVsDomain(clientUrl, competitors, database, apiKey, displayLimit) + + // Extract keyword lists and gap analysis (already computed by domain_domains API) + const clientKeywords = keywordGapData.clientKeywords + const competitorKeywordsList = keywordGapData.competitorKeywordsList + const allKeywords = keywordGapData.allKeywords + + // Calculate totals + const totalCompetitorKeywordsFetched = competitorKeywordsList.reduce((sum, kw) => sum + kw.length, 0) + + console.log('\n[DOMAIN_DOMAINS RESULTS SUMMARY]') + console.log(`Total keywords in comparison: ${allKeywords.length}`) + console.log(`Client (${clientUrl}): ${clientKeywords.length} keywords`) + competitorKeywordsList.forEach((keywords, index) => { + console.log(`Competitor ${index + 1} (${competitors[index]}): ${keywords.length} keywords`) + }) + console.log(`\n══════════════════════════════════════════════════════════`) + console.log(`🎯 GAP ANALYSIS COMPLETE`) + console.log(`══════════════════════════════════════════════════════════`) + console.log(`✅ Total informational keywords: ${allKeywords.length}`) + console.log(`📍 Client keywords: ${clientKeywords.length}`) + console.log(`📍 Competitor keywords (total): ${totalCompetitorKeywordsFetched}`) + console.log(`🎯 Gap keywords (competitors have, client doesn't): ${keywordGapData.gapKeywords.length}`) + console.log(`══════════════════════════════════════════════════════════\n`) + + // Step 2: Store results in structured format + const structuredResults = [ + { domain: clientUrl, keywords: clientKeywords }, + ...competitorKeywordsList.map((keywords, index) => ({ + domain: competitors[index], + keywords: keywords + })) + ] + + // Step 3: Fetch comprehensive domain metrics + const metricsData = await fetchComprehensiveDomainMetrics( + allDomains, + database, + apiKey, + null // Don't pass keywords - let it fetch fresh + ) + + const clientData = metricsData[0] + const competitorsData = metricsData.slice(1) + + // Step 4: Prepare unified analysis data (simplified) + const unifiedData = { + clientUrl: clientUrl, + competitorUrls: competitors, + metrics: metricsData, + structuredResults: structuredResults, + gapKeywords: keywordGapData.gapKeywords, // Simplified - just gap keywords (Intent=1 only) + clientStage: clientData.stage, + hasKeywordOpportunities: keywordGapData.gapKeywords.length > 0 + } + + // Generate and display API unit usage report + const usageReport = getApiUnitReport() + unifiedData.apiUsage = usageReport + + return unifiedData + + } catch (error) { + console.error('Error in unified analysis:', error) + // Still show unit report even on error + const usageReport = getApiUnitReport() + throw error + } +} + +// Fetch comprehensive domain metrics combining all required APIs +// Extracts: Rank, Organic Traffic, Organic Keywords Count, Top 10 Keywords, +// Authority Score, Total Backlinks, Referring Domains Count, Top 10 Referring Domains +// @param keywordsList - Optional pre-fetched keywords list to avoid duplicate domain_organic calls +export const fetchComprehensiveDomainMetrics = async (domains, database, apiKey, keywordsList = null) => { + const results = [] + + for (let i = 0; i < domains.length; i++) { + const domain = domains[i] + console.log(`Fetching comprehensive metrics for: ${domain}`) + + try { + // Fetch all metrics in parallel with proper error handling + const [domainRanks, backlinkOverview, referringDomains, organicKeywordsResult] = await Promise.all([ + fetchDomainRanks(domain, database, apiKey).catch(err => { + console.error(`Domain Ranks error for ${domain}:`, err) + return { error: err.message, rank: 0, organicTraffic: 0, organicKeywordsCount: 0 } + }), + fetchBacklinkOverview(domain, apiKey).catch(err => { + console.error(`Backlink Overview error for ${domain}:`, err) + return { error: err.message, authorityScore: 0, totalBacklinks: 0, referringDomainsCount: 0 } + }), + fetchReferringDomains(domain, apiKey).catch(err => { + console.error(`Referring Domains error for ${domain}:`, err) + return [] + }), + // Fetch top 10 organic keywords (Intent=1 only) + keywordsList && keywordsList[i] + ? Promise.resolve({ top10Keywords: keywordsList[i] }) + : fetchOrganicKeywords(domain, database, apiKey, 10).catch(err => { + console.error(`Organic Keywords error for ${domain}:`, err) + return { top10Keywords: [] } + }) + ]) + + // Extract top 10 keywords from API result + const top10Keywords = organicKeywordsResult?.top10Keywords || [] + + // Extract required data with fallbacks + const rank = (domainRanks && !domainRanks.error) ? (domainRanks.rank ?? 0) : 0 + const organicTraffic = (domainRanks && !domainRanks.error) ? (domainRanks.organicTraffic ?? 0) : 0 + const organicKeywordsCount = (domainRanks && !domainRanks.error) ? (domainRanks.organicKeywordsCount ?? 0) : 0 + + // top10Keywords already set above from pre-fetched keywords + + // Check if backlinkOverview has error before extracting + const backlinkData = (backlinkOverview && !backlinkOverview.error) ? backlinkOverview : null + const authorityScore = backlinkData?.authorityScore ?? 0 + const totalBacklinks = backlinkData?.totalBacklinks ?? 0 + const referringDomainsCount = backlinkData?.referringDomainsCount ?? 0 + const top10ReferringDomains = Array.isArray(referringDomains) ? referringDomains : [] + + console.log('Backlink Overview Data Check:', { + hasBacklinkOverview: !!backlinkOverview, + hasError: backlinkOverview?.error, + authorityScore, + totalBacklinks, + referringDomainsCount, + rawBacklinkData: backlinkData + }) + + console.log(`Metrics extracted for ${domain}:`, { + rank, + organicTraffic, + organicKeywordsCount, + authorityScore, + totalBacklinks, + referringDomainsCount, + keywordsCount: top10Keywords.length, + referringDomainsCount: top10ReferringDomains.length + }) + + // Classify SEO maturity stage based on Authority Score and Organic Keywords Count + const stage = classifySEOMaturity(authorityScore, organicKeywordsCount) + + const comprehensiveData = { + domain: domain, + rank: rank, + organicTraffic: organicTraffic, + organicKeywordsCount: organicKeywordsCount, + top10Keywords: top10Keywords, + authorityScore: authorityScore, + totalBacklinks: totalBacklinks, + referringDomainsCount: referringDomainsCount, + top10ReferringDomains: top10ReferringDomains, + stage: stage + } + + results.push(comprehensiveData) + + } catch (error) { + console.error(`Error fetching comprehensive metrics for "${domain}":`, error) + console.error('Full error stack:', error.stack) + // Still add the result but with error flag + results.push({ + domain: domain, + error: error.message, + rank: 0, + organicTraffic: 0, + organicKeywordsCount: 0, + top10Keywords: [], + authorityScore: 0, + totalBacklinks: 0, + referringDomainsCount: 0, + top10ReferringDomains: [], + stage: 'ERROR' + }) + } + } + + return results +} + +// Fetch domain ranks for a single domain +// Returns: Rank (Rk), Organic Traffic (Ot), Organic Keywords count (Oc) +export const fetchDomainRanks = async (domain, database, apiKey) => { + try { + const params = { + type: 'domain_ranks', + key: apiKey, + domain: domain, + database: database, + export_columns: 'Db,Dn,Rk,Or,Ot,Oc,Ad,At,Ac' + } + + const response = await axios.get(SEMRUSH_API_BASE, { + params, + headers: { + 'Accept': 'text/plain' + } + }) + + if (!response.data || typeof response.data !== 'string') { + throw new Error('Invalid API response format') + } + + if (response.data.includes('ERROR')) { + throw new Error(response.data) + } + + const lines = response.data.trim().split('\n').filter(line => line.trim().length > 0) + if (lines.length < 2) { + throw new Error('No domain ranks data found - insufficient response lines') + } + + const values = lines[1].split(';').map(v => v.trim()) + const headers = lines[0].split(';').map(h => h.trim()) + + const result = {} + headers.forEach((header, index) => { + result[header.toLowerCase()] = values[index] || '0' + }) + + const rank = parseInt(result.rk || result.rank || '0') || 0 + const organicTraffic = parseInt(result.ot || result['organic traffic'] || '0') || 0 + const organicKeywordsCount = parseInt(result.oc || result['organic keywords'] || '0') || 0 + + // Track API units (1 domain = 10 units) + trackApiUnits('domain_ranks', API_UNIT_COSTS.domain_ranks, `Domain: ${domain}`) + + return { + domain: result.dn || result.domain || domain, + rank: rank, + organicTraffic: organicTraffic, + organicKeywordsCount: organicKeywordsCount + } + } catch (error) { + console.error(`Error fetching domain ranks for "${domain}":`, error) + throw error + } +} + +// Fetch organic keywords for a single domain +// Returns: Top organic keywords +// NOTE: This function is deprecated - use fetchOrganicKeywordsForDomain instead +// Fetch top 10 organic keywords (all intents) +// API Reference: https://api.semrush.com/?type=domain_organic +export const fetchOrganicKeywords = async (domain, database, apiKey, displayLimit = 10) => { + try { + const params = { + type: 'domain_organic', + key: apiKey, + domain: domain, + database: database, + display_limit: displayLimit, + display_sort: 'tr_desc', // Sort by traffic descending + export_columns: 'Ph,In,Nq,Kd' // Keyword, Intent, Search Volume, Keyword Difficulty + } + + console.log(`[API] Fetching top ${displayLimit} organic keywords for ${domain}`) + + const response = await axios.get(SEMRUSH_API_BASE, { + params, + headers: { + 'Accept': 'text/plain' + } + }) + + if (!response.data || typeof response.data !== 'string') { + console.warn(`[WARNING] No organic keywords data for ${domain}`) + return { top10Keywords: [] } + } + + if (response.data.includes('ERROR')) { + console.error(`[ERROR] API error for ${domain}:`, response.data) + return { top10Keywords: [] } + } + + const lines = response.data.trim().split('\n').filter(line => line.trim().length > 0) + if (lines.length < 2) { + console.warn(`[WARNING] No organic keywords found for ${domain}`) + return { top10Keywords: [] } + } + + const headers = lines[0].split(';').map(h => h.trim()) + const keywords = [] + + // Track API units (10 units per line returned) + const linesReturned = lines.length - 1 + trackApiUnits('domain_organic', linesReturned * 10, `${domain}: ${linesReturned} keywords`) + + for (let i = 1; i < lines.length; i++) { + const values = lines[i].split(';').map(v => v.trim()) + + // Map: Keyword;Intents;Search Volume;Keyword Difficulty + const keyword = { + phrase: values[0] || '', + intent: parseInt(values[1]) || 1, // Intent (1=Informational) + searchVolume: parseInt(values[2]) || 0, + keywordDifficulty: parseFloat(values[3]) || 0 + } + + keywords.push(keyword) + } + + console.log(`[SUCCESS] Fetched ${keywords.length} organic keywords for ${domain}`) + if (keywords.length > 0) { + console.log(`[SAMPLE] Top 3 keywords:`, keywords.slice(0, 3).map(k => `${k.phrase} (vol: ${k.searchVolume}, intent: ${k.intent})`)) + } + + return { + top10Keywords: keywords + } + } catch (error) { + console.error(`[ERROR] Failed to fetch organic keywords for "${domain}":`, error.message) + return { top10Keywords: [] } + } +} + +// Fetch referring domains for a single domain +// Returns: Top referring domains (default 10) +export const fetchReferringDomains = async (domain, apiKey, displayLimit = 10) => { + try { + const params = { + type: 'backlinks_refdomains', + key: apiKey, + target: domain, + target_type: 'root_domain', + export_columns: 'domain_ascore,domain,backlinks_num', + display_limit: displayLimit + } + + const response = await axios.get(SEMRUSH_ANALYTICS_API_BASE, { + params, + headers: { + 'Accept': 'text/plain' + } + }) + + if (response.data.includes('ERROR')) { + throw new Error(response.data) + } + + const lines = response.data.trim().split('\n') + if (lines.length < 2) { + throw new Error('No referring domains data found') + } + + const headers = lines[0].split(';').map(h => h.trim().replace(/\\_/g, '_')) + const referringDomains = [] + + for (let i = 1; i < lines.length; i++) { + const values = lines[i].split(';').map(v => v.trim()) + const row = {} + + headers.forEach((header, index) => { + const cleanHeader = header.toLowerCase().replace(/\\_/g, '_') + row[cleanHeader] = values[index] || '' + }) + + referringDomains.push({ + domainAscore: parseInt(row.domain_ascore || row['domain_ascore'] || '0') || 0, + domain: row.domain || '', + backlinksNum: parseInt(row.backlinks_num || row['backlinks_num'] || '0') || 0 + }) + } + + // Track API units (10 units per line) + const linesReturned = referringDomains.length + trackApiUnits('backlinks_refdomains', linesReturned * API_UNIT_COSTS.backlinks_refdomains, `${linesReturned} referring domains for ${domain}`) + + return referringDomains // Return all fetched (controlled by displayLimit parameter) + } catch (error) { + console.error(`Error fetching referring domains for "${domain}":`, error) + return [] + } +} + +// Fetch backlink overview for a single domain +// Returns: Authority Score (ascore), Total Backlinks (total), Referring Domains count (domains_num) +export const fetchBacklinkOverview = async (domain, apiKey) => { + try { + const params = { + type: 'backlinks_overview', + key: apiKey, + target: domain, + target_type: 'root_domain', + export_columns: 'ascore,total,domains_num,urls_num,ips_num' + } + + const response = await axios.get(SEMRUSH_ANALYTICS_API_BASE, { + params, + headers: { + 'Accept': 'text/plain' + } + }) + + if (!response.data || typeof response.data !== 'string') { + throw new Error('Invalid API response format') + } + + if (response.data.includes('ERROR')) { + throw new Error(response.data) + } + + const lines = response.data.trim().split('\n').filter(line => line.trim().length > 0) + if (lines.length < 2) { + throw new Error('No backlink data found - insufficient response lines') + } + + // Parse headers and values + // Response format: ascore;total;domains_num;urls_num;ips_num\n29;3775;766;2846;657 + const headerLine = lines[0].trim() + const valueLine = lines[1].trim() + + const headers = headerLine.split(';').map(h => h.trim()) + const values = valueLine.split(';').map(v => v.trim()) + + // Create result object - use exact header names as keys + const result = {} + headers.forEach((header, index) => { + if (header && values[index] !== undefined) { + result[header] = values[index] + // Also store lowercase version + result[header.toLowerCase()] = values[index] + // Handle potential escaped underscores + const cleanHeader = header.replace(/\\_/g, '_') + result[cleanHeader] = values[index] + result[cleanHeader.toLowerCase()] = values[index] + } + }) + + // Extract values - use direct index access as primary method since order is fixed + // Response order: ascore[0], total[1], domains_num[2], urls_num[3], ips_num[4] + // Fallback to result object lookup + const authorityScoreRaw = values[0] || result['ascore'] || result.ascore || '0' + const totalBacklinksRaw = values[1] || result['total'] || result.total || '0' + const referringDomainsRaw = values[2] || result['domains_num'] || result.domains_num || '0' + + const authorityScore = parseInt(authorityScoreRaw, 10) || 0 + const totalBacklinks = parseInt(totalBacklinksRaw, 10) || 0 + const referringDomainsCount = parseInt(referringDomainsRaw, 10) || 0 + + // Track API units (1 domain = 10 units) + trackApiUnits('backlinks_overview', API_UNIT_COSTS.backlinks_overview, `Domain: ${domain}`) + + return { + domain: domain, + authorityScore: authorityScore, + totalBacklinks: totalBacklinks, + referringDomainsCount: referringDomainsCount + } + } catch (error) { + console.error(`Error fetching backlink overview for "${domain}":`, error) + // Return error object instead of throwing so calling code can handle it + return { + error: error.message, + domain: domain, + authorityScore: 0, + totalBacklinks: 0, + referringDomainsCount: 0 + } + } +} + +// Send comprehensive analysis data to Hookpilot webhook +// Properly parsed with key-value pairs for client, competitors, etc. +export const sendToHookpilot = async (analysisData) => { + try { + const webhookUrl = 'https://dashboard.hookpilot.org/api/v1/webhooks/aiJHwxtJr85F56xb0tYPu' + + // Parse metrics data + const clientMetrics = analysisData.metrics[0] || {} + const competitorMetrics = analysisData.metrics.slice(1) || [] + const gapKeywords = analysisData.gapKeywords || [] + + // Structure data with proper key-value pairs + const payload = { + timestamp: new Date().toISOString(), + analysisType: 'SEO Analysis', + // Explicit flag and list for keyword gap analysis + 'keyword-gap-analyzed': true, + + // Client information as key-value pairs + client: { + url: analysisData.clientUrl, + rank: clientMetrics.rank || 0, + organicTraffic: clientMetrics.organicTraffic || 0, + organicKeywordsCount: clientMetrics.organicKeywordsCount || 0, + authorityScore: clientMetrics.authorityScore || 0, + totalBacklinks: clientMetrics.totalBacklinks || 0, + referringDomainsCount: clientMetrics.referringDomainsCount || 0, + stage: clientMetrics.stage || 'Unknown', + top10Keywords: clientMetrics.top10Keywords || [], + top10ReferringDomains: clientMetrics.top10ReferringDomains || [] + }, + + // Competitors as key-value pairs array + competitors: competitorMetrics.map((comp, index) => ({ + url: comp.domain || analysisData.competitorUrls[index] || '', + rank: comp.rank || 0, + organicTraffic: comp.organicTraffic || 0, + organicKeywordsCount: comp.organicKeywordsCount || 0, + authorityScore: comp.authorityScore || 0, + totalBacklinks: comp.totalBacklinks || 0, + referringDomainsCount: comp.referringDomainsCount || 0, + stage: comp.stage || 'Unknown', + top10Keywords: comp.top10Keywords || [], + top10ReferringDomains: comp.top10ReferringDomains || [] + })), + + // Keyword gap analysis - Informational keywords (Intent=1) + // Keywords where competitors rank but client doesn't + keywordgapanalyzed: gapKeywords.map(keyword => ({ + keyword: keyword.keyword, + intent: keyword.intentLabel || 'Informational', + searchVolume: keyword.volume || 0, + keywordDifficulty: keyword.kd || 0, + competitors: keyword.competitor_domains || [], + competitorsFoundIn: keyword.competitors_found_in || '' + })) + } + + console.log('Sending data to Hookpilot:', payload) + + const response = await axios.post(webhookUrl, payload, { + headers: { + 'Content-Type': 'application/json' + }, + timeout: 15000 // 15 second timeout + }) + + console.log('Hookpilot response:', response.status, response.data) + return { success: true, response: response.data } + } catch (error) { + console.error('Error sending to Hookpilot:', error) + return { success: false, error: error.message } + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..fee9ffa --- /dev/null +++ b/vite.config.js @@ -0,0 +1,46 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + proxy: { + '/api/semrush': { + target: 'https://api.semrush.com', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/semrush/, ''), + configure: (proxy, options) => { + proxy.on('proxyReq', (proxyReq, req, res) => { + // Add CORS headers to the request + proxyReq.setHeader('Origin', 'https://api.semrush.com'); + }); + proxy.on('proxyRes', (proxyRes, req, res) => { + // Add CORS headers to the response + proxyRes.headers['Access-Control-Allow-Origin'] = '*'; + proxyRes.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'; + proxyRes.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'; + }); + } + }, + '/api/analytics': { + target: 'https://api.semrush.com', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/analytics/, '/analytics'), + configure: (proxy, options) => { + proxy.on('proxyReq', (proxyReq, req, res) => { + // Add CORS headers to the request + proxyReq.setHeader('Origin', 'https://api.semrush.com'); + }); + proxy.on('proxyRes', (proxyRes, req, res) => { + // Add CORS headers to the response + proxyRes.headers['Access-Control-Allow-Origin'] = '*'; + proxyRes.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'; + proxyRes.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'; + }); + } + } + } + } +}) +