initial-commit

This commit is contained in:
jassim 2025-11-04 17:40:16 +05:30
commit 6e82a55219
24 changed files with 9851 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules
dist
.DS_Store
*.log
.env
.env.local

286
API_QUICK_REFERENCE.md Normal file
View File

@ -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

351
README.md Normal file
View File

@ -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**

201
SIMPLIFIED_FLOW.md Normal file
View File

@ -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!

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SEMrush Blog Keyword Research</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

1909
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@ -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"
}
}

View File

@ -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": []
}
]
}

181
src/App.css Normal file
View File

@ -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;
}
}

98
src/App.jsx Normal file
View File

@ -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 (
<div className="app">
<div className="app-header">
<h1>🚀 Unified SEO Analysis Tool</h1>
<p>Comprehensive metrics + Keyword research + Industry detection + Hookpilot integration</p>
</div>
<KeywordForm
onSearch={handleSearch}
loading={loading}
/>
{loading && (
<div className="progress-container">
<div className="progress-bar">
<div
className="progress-fill"
style={{
width: `${progress.total > 0 ? (progress.current / progress.total) * 100 : 0}%`
}}
/>
</div>
<p className="progress-text">
{progress.total > 0
? `Analyzing ${progress.total} operations (Keywords validation + Domain metrics)...`
: 'Initializing analysis...'}
</p>
</div>
)}
{error && (
<div className="error-message">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" fill="currentColor"/>
</svg>
{error}
</div>
)}
{unifiedAnalysis && (
<UnifiedAnalysis
analysisData={unifiedAnalysis}
/>
)}
</div>
)
}
export default App

View File

@ -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;
}
}

View File

@ -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 <span className="no-data">No keywords data</span>
}
return (
<div className="keywords-list">
{keywords.slice(0, 5).map((keyword, index) => (
<div key={index} className="keyword-item">
<span className="keyword-phrase">{keyword.phrase}</span>
<span className="keyword-metrics">
SV: {keyword.searchVolume?.toLocaleString() || 'N/A'} |
KD: {keyword.keywordDifficulty || 'N/A'} |
Pos: {keyword.position || 'N/A'}
</span>
</div>
))}
{keywords.length > 5 && (
<div className="more-keywords">+{keywords.length - 5} more keywords</div>
)}
</div>
)
}
const renderTopCategories = (categories) => {
if (!categories || categories.length === 0) {
return <span className="no-data">No categories data</span>
}
return (
<div className="categories-list">
{categories.map((category, index) => (
<div key={index} className="category-item">
<span className="category-url">{category.url}</span>
<span className="category-traffic">{category.traffic?.toLocaleString() || 0} visits</span>
</div>
))}
</div>
)
}
const renderDetailedView = (domainData) => {
if (!domainData || domainData.error) return null
return (
<div className="detailed-view">
<div className="detailed-sections">
<div className="section">
<h4>📊 Complete Metrics</h4>
<div className="metrics-grid">
<div className="metric-item">
<span className="metric-label">Authority Score:</span>
<span className="metric-value">{domainData.authorityScore}</span>
</div>
<div className="metric-item">
<span className="metric-label">Organic Traffic:</span>
<span className="metric-value">{domainData.organicTraffic?.toLocaleString()}</span>
</div>
<div className="metric-item">
<span className="metric-label">Organic Keywords:</span>
<span className="metric-value">{domainData.organicKeywords?.toLocaleString()}</span>
</div>
<div className="metric-item">
<span className="metric-label">Paid Traffic:</span>
<span className="metric-value">{domainData.paidTraffic?.toLocaleString()}</span>
</div>
<div className="metric-item">
<span className="metric-label">Paid Keywords:</span>
<span className="metric-value">{domainData.paidKeywords?.toLocaleString()}</span>
</div>
<div className="metric-item">
<span className="metric-label">Total Backlinks:</span>
<span className="metric-value">{domainData.totalBacklinks?.toLocaleString()}</span>
</div>
</div>
</div>
<div className="section">
<h4>🔍 Top 10 Keywords</h4>
<div className="keywords-detailed">
{domainData.top10Keywords?.map((keyword, index) => (
<div key={index} className="keyword-detailed">
<div className="keyword-main">
<span className="keyword-rank">#{index + 1}</span>
<span className="keyword-text">{keyword.phrase}</span>
</div>
<div className="keyword-stats">
<span>SV: {keyword.searchVolume?.toLocaleString() || 'N/A'}</span>
<span>KD: {keyword.keywordDifficulty || 'N/A'}</span>
<span>Pos: {keyword.position || 'N/A'}</span>
<span>CPC: ${keyword.cpc?.toFixed(2) || 'N/A'}</span>
</div>
</div>
))}
</div>
</div>
<div className="section">
<h4>📄 Top Categories/Pages</h4>
<div className="categories-detailed">
{domainData.topCategories?.map((category, index) => (
<div key={index} className="category-detailed">
<div className="category-url">{category.url}</div>
<div className="category-stats">
<span>Traffic: {category.traffic?.toLocaleString() || 0}</span>
<span>Keywords: {category.positionCount || 1}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}
return (
<div className="comprehensive-metrics">
<div className="metrics-header">
<div className="header-content">
<h2>📊 Comprehensive Domain Metrics</h2>
<p>Complete SEO analysis with all required metrics for SEO maturity assessment</p>
</div>
{hookpilotStatus && (
<div className={`hookpilot-status ${hookpilotStatus.success ? 'success' : 'error'}`}>
{hookpilotStatus.success ? '✅ Sent to Hookpilot' : '❌ Hookpilot Error'}
</div>
)}
</div>
<div className="metrics-table-container">
<table className="metrics-table">
<thead>
<tr>
<th>Actions</th>
<th>Domain</th>
<th>Authority Score</th>
<th>Organic Keywords</th>
<th>Top 10 Keywords</th>
<th>Referring Domains</th>
<th>Monthly Traffic</th>
<th>Top Categories</th>
<th>Stage</th>
</tr>
</thead>
<tbody>
{metricsData.map((domainData, index) => (
<React.Fragment key={index}>
<tr className={domainData.error ? 'error-row' : ''}>
<td className="actions-cell">
{!domainData.error && (
<button
className="expand-button"
onClick={() => toggleDomainDetails(domainData.domain)}
title="View detailed metrics"
>
{expandedDomain === domainData.domain ? '▼' : '▶'}
</button>
)}
</td>
<td className="domain-cell">
<strong>{domainData.domain}</strong>
{index === 0 && <span className="client-badge">Client</span>}
</td>
<td className="metric-cell">
{domainData.error ? 'Error' : domainData.authorityScore?.toLocaleString()}
</td>
<td className="metric-cell">
{domainData.error ? 'Error' : domainData.organicKeywords?.toLocaleString()}
</td>
<td className="keywords-cell">
{domainData.error ? 'Error' : renderTopKeywords(domainData.top10Keywords)}
</td>
<td className="metric-cell">
{domainData.error ? 'Error' : domainData.referringDomains?.toLocaleString()}
</td>
<td className="metric-cell">
{domainData.error ? 'Error' : domainData.monthlyTraffic?.toLocaleString()}
</td>
<td className="categories-cell">
{domainData.error ? 'Error' : renderTopCategories(domainData.topCategories)}
</td>
<td className="stage-cell">
<span
className="stage-badge"
style={{ backgroundColor: getStageColor(domainData.stage) }}
>
{getStageIcon(domainData.stage)} {domainData.stage}
</span>
</td>
</tr>
{expandedDomain === domainData.domain && !domainData.error && (
<tr className="expanded-row">
<td colSpan="9">
{renderDetailedView(domainData)}
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
</div>
{/* Classification Legend */}
<div className="classification-legend">
<h4>📋 SEO Maturity Classification Criteria</h4>
<div className="legend-items">
<div className="legend-item">
<span className="legend-color" style={{ backgroundColor: '#ff6b6b' }}></span>
<strong>NEW:</strong> AS &lt; 20 OR Organic KW &lt; 500 OR &lt; 10 Ref Domains
</div>
<div className="legend-item">
<span className="legend-color" style={{ backgroundColor: '#ffa726' }}></span>
<strong>GROWING:</strong> AS 2040 OR Organic KW 5003000
</div>
<div className="legend-item">
<span className="legend-color" style={{ backgroundColor: '#66bb6a' }}></span>
<strong>ESTABLISHED:</strong> AS &gt; 40 OR Organic KW &gt; 3000 OR &gt; 100 Ref Domains
</div>
</div>
</div>
</div>
)
}
export default ComprehensiveMetrics

View File

@ -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;
}
}

View File

@ -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 (
<form className="keyword-form" onSubmit={handleSubmit}>
<div className="form-card">
<div className="form-section">
<h2>Website Information</h2>
<div className="form-group">
<label htmlFor="clientUrl">
Client Website URL *
</label>
<input
type="text"
id="clientUrl"
name="clientUrl"
placeholder="evendigit.com"
value={formData.clientUrl}
onChange={handleChange}
disabled={loading}
required
/>
</div>
<div className="competitors-section">
<h3>Competitor URLs (1-4)</h3>
<div className="form-group">
<label htmlFor="competitor1">Competitor 1 *</label>
<input
type="text"
id="competitor1"
name="competitor1"
placeholder="techmagnate.com"
value={formData.competitor1}
onChange={handleChange}
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="competitor2">Competitor 2</label>
<input
type="text"
id="competitor2"
name="competitor2"
placeholder="persuasionexperience.com"
value={formData.competitor2}
onChange={handleChange}
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="competitor3">Competitor 3</label>
<input
type="text"
id="competitor3"
name="competitor3"
placeholder="infidigit.com"
value={formData.competitor3}
onChange={handleChange}
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="competitor4">Competitor 4</label>
<input
type="text"
id="competitor4"
name="competitor4"
placeholder="ignitevisibility.com"
value={formData.competitor4}
onChange={handleChange}
disabled={loading}
/>
</div>
</div>
</div>
<div className="form-section">
<h2>Search Parameters</h2>
<div className="form-row">
<div className="form-group">
<label htmlFor="database">Target Location / Market *</label>
<select
id="database"
name="database"
value={formData.database}
onChange={handleChange}
disabled={loading}
>
{DATABASES.map(db => (
<option key={db.value} value={db.value}>
{db.label}
</option>
))}
</select>
</div>
<div className="form-group">
<label htmlFor="displayLimit">Display Limit *</label>
<input
type="number"
id="displayLimit"
name="displayLimit"
min="1"
max="10000"
value={formData.displayLimit}
onChange={handleChange}
disabled={loading}
/>
</div>
</div>
<div className="form-group">
<label htmlFor="apiKey">SEMrush API Key *</label>
<input
type="text"
id="apiKey"
name="apiKey"
placeholder="Your SEMrush API key"
value={formData.apiKey}
onChange={handleChange}
disabled={loading}
/>
</div>
</div>
<button type="submit" className="submit-btn" disabled={loading}>
{loading ? (
<>
<span className="spinner"></span>
Processing...
</>
) : (
<>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M8.5 14L4.5 10L5.91 8.59L8.5 11.17L14.59 5.08L16 6.5L8.5 14Z" fill="currentColor"/>
</svg>
Run Unified Analysis
</>
)}
</button>
</div>
</form>
)
}
export default KeywordForm

View File

@ -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;
}
}

View File

@ -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 (
<div className="keyword-results">
<div className="results-header">
<div className="results-title">
<h2>Informational Keywords Found</h2>
<span className="results-count">{keywords.length} keywords</span>
</div>
<button onClick={exportToCSV} className="export-btn">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M14 8L10 12L6 8H9V4H11V8H14ZM4 14V16H16V14H4Z" fill="currentColor"/>
</svg>
Export CSV
</button>
</div>
<div className="table-container">
<table className="keywords-table">
<thead>
<tr>
<th onClick={() => handleSort('phrase')} className="sortable">
Keyword
{sortBy === 'phrase' && (
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</th>
<th onClick={() => handleSort('volume')} className="sortable">
Search Volume
{sortBy === 'volume' && (
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</th>
<th onClick={() => handleSort('difficulty')} className="sortable">
Difficulty
{sortBy === 'difficulty' && (
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</th>
<th onClick={() => handleSort('cpc')} className="sortable">
CPC
{sortBy === 'cpc' && (
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</th>
<th>Competition</th>
<th>Intent</th>
</tr>
</thead>
<tbody>
{sortedKeywords.map((keyword, index) => (
<tr key={index} className="keyword-row">
<td className="keyword-phrase">{keyword.phrase}</td>
<td className="keyword-volume">
{keyword.volume ? parseInt(keyword.volume).toLocaleString() : 'N/A'}
</td>
<td className="keyword-difficulty">
<div className="difficulty-cell">
<span className={`difficulty-badge difficulty-${getDifficultyLevel(keyword.difficulty)}`}>
{keyword.difficulty || 'N/A'}
</span>
</div>
</td>
<td className="keyword-cpc">
{keyword.cpc ? `${keyword.cpc}` : 'N/A'}
</td>
<td className="keyword-competition">
{keyword.competition || 'N/A'}
</td>
<td className="keyword-intent">
<span className="intent-badge">Informational</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
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

View File

@ -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;
}
}

View File

@ -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 (
<div className="metrics-cards">
<div className="metric-card authority">
<div className="metric-icon">🏆</div>
<div className="metric-content">
<div className="metric-value">{clientData.authorityScore}</div>
<div className="metric-label">Authority Score</div>
<div className="metric-stage">{clientData.stage}</div>
</div>
</div>
<div className="metric-card keywords">
<div className="metric-icon">🔍</div>
<div className="metric-content">
<div className="metric-value">{clientData.organicKeywords.toLocaleString()}</div>
<div className="metric-label">Organic Keywords</div>
<div className="metric-subtitle">Ranking positions</div>
</div>
</div>
<div className="metric-card traffic">
<div className="metric-icon">📈</div>
<div className="metric-content">
<div className="metric-value">{clientData.organicTraffic.toLocaleString()}</div>
<div className="metric-label">Organic Traffic</div>
<div className="metric-subtitle">Monthly visits</div>
</div>
</div>
<div className="metric-card backlinks">
<div className="metric-icon">🔗</div>
<div className="metric-content">
<div className="metric-value">{clientData.referringDomains.toLocaleString()}</div>
<div className="metric-label">Referring Domains</div>
<div className="metric-subtitle">Backlink sources</div>
</div>
</div>
</div>
)
}
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 (
<div className="detailed-data-container">
<div className="detailed-tabs">
<button
className={`tab-button ${activeTab === 'keywords' ? 'active' : ''}`}
onClick={() => setActiveTab('keywords')}
>
🔍 Organic Keywords ({data.topKeywords?.length || 0})
</button>
<button
className={`tab-button ${activeTab === 'backlinks' ? 'active' : ''}`}
onClick={() => setActiveTab('backlinks')}
>
🔗 Backlinks ({data.backlinksList?.length || 0})
</button>
<button
className={`tab-button ${activeTab === 'pages' ? 'active' : ''}`}
onClick={() => setActiveTab('pages')}
>
📄 Top Pages ({data.topPages?.length || 0})
</button>
</div>
<div className="tab-content">
{activeTab === 'keywords' && (
<div className="keywords-table-container">
<table className="detailed-table">
<thead>
<tr>
<th>Keyword</th>
<th>Position</th>
<th>Search Volume</th>
<th>CPC</th>
<th>Competition</th>
<th>Keyword Difficulty</th>
</tr>
</thead>
<tbody>
{data.topKeywords?.map((kw, idx) => (
<tr key={idx}>
<td className="keyword-cell">{kw.phrase}</td>
<td className="position-cell">{kw.position}</td>
<td className="volume-cell">{kw.searchVolume?.toLocaleString()}</td>
<td className="cpc-cell">${kw.cpc?.toFixed(2)}</td>
<td className="competition-cell">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${(kw.competition * 100).toFixed(0)}%` }}
></div>
<span className="progress-text">{(kw.competition * 100).toFixed(0)}%</span>
</div>
</td>
<td className="difficulty-cell">
<span className={`difficulty-badge difficulty-${Math.floor(kw.keywordDifficulty / 25)}`}>
{kw.keywordDifficulty}
</span>
</td>
</tr>
))}
{(!data.topKeywords || data.topKeywords.length === 0) && (
<tr>
<td colSpan="6" className="no-data">No organic keywords data available</td>
</tr>
)}
</tbody>
</table>
</div>
)}
{activeTab === 'backlinks' && (
<div className="backlinks-table-container">
<div className="backlinks-summary">
<div className="summary-stat">
<span className="stat-label">Total Backlinks:</span>
<span className="stat-value">{data.totalBacklinks?.toLocaleString()}</span>
</div>
<div className="summary-stat">
<span className="stat-label">Referring Domains:</span>
<span className="stat-value">{data.referringDomains?.toLocaleString()}</span>
</div>
<div className="summary-stat">
<span className="stat-label">Referring URLs:</span>
<span className="stat-value">{data.referringUrls?.toLocaleString()}</span>
</div>
<div className="summary-stat">
<span className="stat-label">Referring IPs:</span>
<span className="stat-value">{data.referringIps?.toLocaleString()}</span>
</div>
</div>
<table className="detailed-table backlinks-table">
<thead>
<tr>
<th>Page Authority</th>
<th>Source URL</th>
<th>Target URL</th>
<th>Anchor Text</th>
</tr>
</thead>
<tbody>
{data.backlinksList?.map((bl, idx) => (
<tr key={idx}>
<td className="ascore-cell">
<span className={`ascore-badge ascore-${Math.floor(bl.pageAscore / 25)}`}>
{bl.pageAscore}
</span>
</td>
<td className="url-cell">
<a href={bl.sourceUrl} target="_blank" rel="noopener noreferrer" title={bl.sourceUrl}>
{bl.sourceUrl.length > 60 ? bl.sourceUrl.substring(0, 60) + '...' : bl.sourceUrl}
</a>
</td>
<td className="url-cell">
<a href={bl.targetUrl} target="_blank" rel="noopener noreferrer" title={bl.targetUrl}>
{bl.targetUrl.length > 60 ? bl.targetUrl.substring(0, 60) + '...' : bl.targetUrl}
</a>
</td>
<td className="anchor-cell">{bl.anchor || '(no anchor)'}</td>
</tr>
))}
{(!data.backlinksList || data.backlinksList.length === 0) && (
<tr>
<td colSpan="4" className="no-data">No backlinks data available</td>
</tr>
)}
</tbody>
</table>
</div>
)}
{activeTab === 'pages' && (
<div className="pages-table-container">
<table className="detailed-table">
<thead>
<tr>
<th>Page URL</th>
<th>Traffic</th>
<th>Keywords Count</th>
</tr>
</thead>
<tbody>
{data.topPages?.map((page, idx) => (
<tr key={idx}>
<td className="url-cell">
<a href={page.url} target="_blank" rel="noopener noreferrer" title={page.url}>
{page.url}
</a>
</td>
<td className="traffic-cell">{page.traffic?.toLocaleString()}</td>
<td className="count-cell">{page.positionCount || 1}</td>
</tr>
))}
{(!data.topPages || data.topPages.length === 0) && (
<tr>
<td colSpan="3" className="no-data">No top pages data available</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
</div>
)
}
return (
<div className="seo-maturity-analysis">
<div className="analysis-header">
<h2>SEO Maturity Analysis</h2>
<p>Comprehensive domain-level metrics and classification</p>
</div>
{/* Key Metrics Cards */}
{getMetricCards()}
{/* Summary Section */}
<div className="summary-section">
<h3>Executive Summary</h3>
<div className="summary-content">
{generateSummary()}
</div>
</div>
{/* Results Table */}
<div className="results-table-container">
<table className="seo-maturity-table">
<thead>
<tr>
<th>Actions</th>
<th>Domain</th>
<th>Authority Score</th>
<th>Organic Keywords</th>
<th>Total Backlinks</th>
<th>Referring Domains</th>
<th>Monthly Traffic</th>
<th>Stage</th>
</tr>
</thead>
<tbody>
{analysisResults.map((result, index) => (
<React.Fragment key={index}>
<tr className={result.error ? 'error-row' : ''}>
<td className="actions-cell">
{!result.error && (
<button
className="expand-button"
onClick={() => toggleDomainDetails(result.domain)}
title="View detailed data"
>
{expandedDomain === result.domain ? '▼' : '▶'}
</button>
)}
</td>
<td className="domain-cell">
<strong>{result.domain}</strong>
{index === 0 && <span className="client-badge">Client</span>}
</td>
<td className="metric-cell">
{result.error ? 'Error' : result.authorityScore.toLocaleString()}
</td>
<td className="metric-cell">
{result.error ? 'Error' : result.organicKeywords.toLocaleString()}
</td>
<td className="metric-cell">
{result.error ? 'Error' : result.totalBacklinks.toLocaleString()}
</td>
<td className="metric-cell">
{result.error ? 'Error' : result.referringDomains.toLocaleString()}
</td>
<td className="metric-cell">
{result.error ? 'Error' : result.monthlyTraffic.toLocaleString()}
</td>
<td className="stage-cell">
<span
className="stage-badge"
style={{ backgroundColor: getStageColor(result.stage) }}
>
{getStageIcon(result.stage)} {result.stage}
</span>
</td>
</tr>
{expandedDomain === result.domain && !result.error && (
<tr className="expanded-row">
<td colSpan="8">
{renderDetailedData(result)}
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
</div>
{/* Classification Legend */}
<div className="classification-legend">
<h4>Classification Criteria</h4>
<div className="legend-items">
<div className="legend-item">
<span className="legend-color" style={{ backgroundColor: '#ff6b6b' }}></span>
<strong>NEW:</strong> AS &lt; 20 OR Organic KW &lt; 500 OR &lt; 10 Ref Domains
</div>
<div className="legend-item">
<span className="legend-color" style={{ backgroundColor: '#ffa726' }}></span>
<strong>GROWING:</strong> AS 2040 OR Organic KW 5003000
</div>
<div className="legend-item">
<span className="legend-color" style={{ backgroundColor: '#66bb6a' }}></span>
<strong>ESTABLISHED:</strong> AS &gt; 40 OR Organic KW &gt; 3000 OR &gt; 100 Ref Domains
</div>
</div>
</div>
</div>
)
}
export default SEOMaturityAnalysis

File diff suppressed because it is too large Load Diff

View File

@ -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 (
<div className="unified-analysis">
{/* Header */}
<div className="analysis-header">
<div className="header-content">
<h2>🚀 SEO Analysis Results</h2>
<p>Domain metrics, keyword gap analysis, and SEO maturity classification</p>
</div>
<div className="header-action-group">
<button
onClick={handleSendToHookpilot}
className={`hookpilot-btn ${sending ? 'sending' : ''} ${hookpilotStatus?.success ? 'success' : ''}`}
disabled={sending}
>
{sending ? (
<>
<span className="button-spinner"></span>
<span>Sending to Hookpilot...</span>
</>
) : hookpilotStatus?.success ? (
<>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 18a8 8 0 100-16 8 8 0 000 16z" fill="#10b981"/>
<path d="M14.707 7.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L10 10.586l3.293-3.293a1 1 0 011.414 0z" fill="white"/>
</svg>
<span>Sent Successfully!</span>
</>
) : (
<>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" fill="currentColor"/>
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" fill="currentColor"/>
</svg>
<span>Send to Hookpilot</span>
</>
)}
</button>
{hookpilotStatus && !hookpilotStatus.success && (
<div className="hookpilot-status error">
<svg width="16" height="16" viewBox="0 0 20 20" fill="none">
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" fill="currentColor"/>
</svg>
{hookpilotStatus.error || 'Failed to send'}
</div>
)}
</div>
</div>
{/* API Usage Report */}
{apiUsage && (
<div style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
borderRadius: '12px',
padding: '24px',
marginBottom: '32px',
color: 'white',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
}}>
<h3 style={{ margin: '0 0 16px 0', fontSize: '20px', fontWeight: '700' }}>
📊 API Usage & Cost Report
</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '24px' }}>
<div>
<div style={{ fontSize: '36px', fontWeight: '800' }}>{apiUsage.totalUnits?.toLocaleString() || 0}</div>
<div style={{ fontSize: '14px', opacity: 0.9 }}>Total Units</div>
</div>
<div>
<div style={{ fontSize: '36px', fontWeight: '800' }}>{apiUsage.duration || '0s'}</div>
<div style={{ fontSize: '14px', opacity: 0.9 }}>Duration</div>
</div>
</div>
{apiUsage.breakdown && apiUsage.breakdown.phrase_this && (
<div style={{
marginTop: '16px',
padding: '12px',
background: 'rgba(255,255,255,0.15)',
borderRadius: '8px'
}}>
<strong>🔍 phrase_this Validation:</strong> {apiUsage.breakdown.phrase_this.calls || 0} API calls, {apiUsage.breakdown.phrase_this.units?.toLocaleString() || 0} units consumed
</div>
)}
</div>
)}
{/* Keyword Gap Analysis Section */}
<div className="keywords-section">
<div className="keywords-header">
<h3>🎯 Informational Gap Keywords (Intent=1)</h3>
<div className="sort-controls">
<span>Sort by:</span>
<select
value={sortBy}
onChange={(e) => handleSort(e.target.value)}
className="sort-select"
>
<option value="volume">Volume</option>
<option value="difficulty">KD</option>
<option value="keyword">Keyword</option>
</select>
<button
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
className="sort-order-btn"
>
{sortOrder === 'asc' ? '↑ Asc' : '↓ Desc'}
</button>
</div>
</div>
{/* Warning message if no keyword opportunities */}
{!hasKeywordOpportunities && (
<div className="warning-message" style={{
padding: '16px 20px',
marginBottom: '24px',
backgroundColor: '#fef3c7',
border: '1px solid #fbbf24',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
gap: '12px'
}}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 18a8 8 0 100-16 8 8 0 000 16zM9 9a1 1 0 012 0v4a1 1 0 11-2 0V9zm1-4a1 1 0 100 2 1 1 0 000-2z" fill="#f59e0b"/>
</svg>
<span style={{ color: '#92400e', fontWeight: '500' }}>
No keyword opportunities found. All competitors may have similar keyword profiles to your client.
</span>
</div>
)}
{/* Simplified Header - Just Informational Keywords */}
{hasKeywordOpportunities && (
<div style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: '20px 24px',
borderRadius: '12px',
color: 'white',
marginBottom: '20px',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '16px' }}>
<div>
<h3 style={{ margin: 0, fontSize: '20px', fontWeight: '700' }}>
🎯 Informational Gap Keywords (Intent=1)
</h3>
<p style={{ margin: '6px 0 0 0', fontSize: '14px', opacity: 0.95 }}>
Keywords competitors rank for, client doesn't - verified as Informational via phrase_this API
</p>
</div>
<div style={{
fontSize: '48px',
fontWeight: '800',
textAlign: 'center',
lineHeight: 1
}}>
{sortedKeywords.length}
</div>
</div>
</div>
)}
{hasKeywordOpportunities && (
<>
<div className="keywords-stats" style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', marginBottom: '20px' }}>
<button onClick={exportMissingKeywords} className="export-btn" style={{ marginLeft: 'auto' }}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M14 8L10 12L6 8H9V4H11V8H14ZM4 14V16H16V14H4Z" fill="currentColor"/>
</svg>
Export CSV
</button>
</div>
<div className="table-container">
<table className="keywords-table">
<thead>
<tr>
<th onClick={() => handleSort('keyword')} className="sortable">
Keyword
{sortBy === 'keyword' && (
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</th>
<th onClick={() => handleSort('volume')} className="sortable">
Volume
{sortBy === 'volume' && (
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</th>
<th onClick={() => handleSort('difficulty')} className="sortable">
KD
{sortBy === 'difficulty' && (
<span className="sort-indicator">{sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</th>
<th>Competitors Found In</th>
</tr>
</thead>
<tbody>
{paginatedKeywords.length === 0 ? (
<tr>
<td colSpan={4} style={{ textAlign: 'center', padding: '40px' }}>
<p style={{ color: '#6b7280', fontSize: '16px' }}>No keywords found in this category.</p>
</td>
</tr>
) : (
paginatedKeywords.map((keyword, index) => (
<tr key={startIndex + index} className="keyword-row">
<td className="keyword-phrase">{keyword.keyword}</td>
<td className="keyword-volume">
{keyword.volume ? formatNumber(keyword.volume) : 'N/A'}
</td>
<td className="keyword-difficulty">
<div className="difficulty-cell">
<span className={`difficulty-badge difficulty-${getDifficultyLevel(keyword.kd)}`}>
{keyword.kd || 'N/A'}
</span>
</div>
</td>
<td className="competitor-count">
{keyword.competitors_found_in || (keyword.competitor_domains && keyword.competitor_domains.join(', ')) || 'N/A'}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination Controls */}
{hasKeywordOpportunities && sortedKeywords.length > 0 && (
<div className="pagination-container">
<div className="pagination-info">
<span>
Showing {startIndex + 1} to {Math.min(endIndex, sortedKeywords.length)} of {sortedKeywords.length} keywords
</span>
<div className="pagination-items-per-page">
<label htmlFor="itemsPerPage">Items per page:</label>
<select
id="itemsPerPage"
value={itemsPerPage}
onChange={handleItemsPerPageChange}
className="items-per-page-select"
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div className="pagination-controls">
<button
className="pagination-btn"
onClick={() => handlePageChange(1)}
disabled={currentPage === 1}
title="First page"
>
««
</button>
<button
className="pagination-btn"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
title="Previous page"
>
Prev
</button>
<div className="pagination-pages">
{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 (
<React.Fragment key={page}>
{showEllipsis && (
<span className="pagination-ellipsis">...</span>
)}
<button
className={`pagination-page-btn ${currentPage === page ? 'active' : ''}`}
onClick={() => handlePageChange(page)}
>
{page}
</button>
</React.Fragment>
)
})}
</div>
<button
className="pagination-btn"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
title="Next page"
>
Next
</button>
<button
className="pagination-btn"
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
title="Last page"
>
»»
</button>
</div>
</div>
)}
</>
)}
</div>
{/* Domain Metrics Section */}
<div className="metrics-section">
<h3>📊 Domain Metrics</h3>
<div className="metrics-grid">
{/* Client Metrics */}
<div className="metric-card client-card">
<div
className="card-header"
onClick={() => openDomainModal(clientData)}
style={{ cursor: 'pointer', userSelect: 'none' }}
>
<h4>{clientData.domain}</h4>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span className="client-badge">Client</span>
<span style={{ fontSize: '20px', opacity: 0.7 }}>👁</span>
</div>
</div>
<div className="metric-content">
<div className="metric-item">
<span className="metric-label">Rank:</span>
<span className="metric-value">{formatNumber(clientData.rank)}</span>
</div>
<div className="metric-item">
<span className="metric-label">Organic Traffic:</span>
<span className="metric-value">{formatNumber(clientData.organicTraffic)}</span>
</div>
<div className="metric-item">
<span className="metric-label">Organic Keywords:</span>
<span className="metric-value">{formatNumber(clientData.organicKeywordsCount)}</span>
</div>
<div className="metric-item">
<span className="metric-label">Authority Score:</span>
<span className="metric-value">{formatNumber(clientData.authorityScore)}</span>
</div>
<div className="metric-item">
<span className="metric-label">Total Backlinks:</span>
<span className="metric-value">{formatNumber(clientData.totalBacklinks)}</span>
</div>
<div className="metric-item">
<span className="metric-label">Referring Domains:</span>
<span className="metric-value">{formatNumber(clientData.referringDomainsCount)}</span>
</div>
<div className="metric-item stage-item">
<span className="metric-label">SEO Stage:</span>
<span
className="stage-badge"
style={{ backgroundColor: getStageColor(clientData.stage) }}
>
{getStageIcon(clientData.stage)} {clientData.stage}
</span>
</div>
</div>
</div>
{/* Competitors Metrics */}
{competitorsData.map((competitor, index) => (
<div key={index} className="metric-card competitor-card">
<div
className="card-header"
onClick={() => openDomainModal(competitor)}
style={{ cursor: 'pointer', userSelect: 'none' }}
>
<h4>{competitor.domain}</h4>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span className="competitor-badge">Competitor {index + 1}</span>
<span style={{ fontSize: '20px', opacity: 0.7 }}>👁</span>
</div>
</div>
<div className="metric-content">
<div className="metric-item">
<span className="metric-label">Rank:</span>
<span className="metric-value">{formatNumber(competitor.rank)}</span>
</div>
<div className="metric-item">
<span className="metric-label">Organic Traffic:</span>
<span className="metric-value">{formatNumber(competitor.organicTraffic)}</span>
</div>
<div className="metric-item">
<span className="metric-label">Organic Keywords:</span>
<span className="metric-value">{formatNumber(competitor.organicKeywordsCount)}</span>
</div>
<div className="metric-item">
<span className="metric-label">Authority Score:</span>
<span className="metric-value">{formatNumber(competitor.authorityScore)}</span>
</div>
<div className="metric-item">
<span className="metric-label">Total Backlinks:</span>
<span className="metric-value">{formatNumber(competitor.totalBacklinks)}</span>
</div>
<div className="metric-item">
<span className="metric-label">Referring Domains:</span>
<span className="metric-value">{formatNumber(competitor.referringDomainsCount)}</span>
</div>
<div className="metric-item stage-item">
<span className="metric-label">SEO Stage:</span>
<span
className="stage-badge"
style={{ backgroundColor: getStageColor(competitor.stage) }}
>
{getStageIcon(competitor.stage)} {competitor.stage}
</span>
</div>
</div>
</div>
))}
</div>
</div>
{/* Domain Details Modal */}
{selectedDomain && (
<div
className="modal-overlay"
onClick={closeDomainModal}
>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
>
<div className="modal-header">
<h3>📊 {selectedDomain.domain} - Details</h3>
<button className="modal-close" onClick={closeDomainModal}></button>
</div>
<div className="modal-body">
{/* Top 10 Organic Keywords */}
{selectedDomain.top10Keywords && selectedDomain.top10Keywords.length > 0 && (
<div className="modal-section">
<h4>
📊 Top 10 Organic Keywords
</h4>
<div className="modal-table-wrapper">
<table className="modal-table">
<thead>
<tr>
<th style={{ textAlign: 'left' }}>Keyword</th>
<th style={{ textAlign: 'right' }}>Search Volume</th>
<th style={{ textAlign: 'right' }}>KD</th>
</tr>
</thead>
<tbody>
{selectedDomain.top10Keywords.map((kw, idx) => (
<tr key={idx}>
<td style={{ fontWeight: '500' }}>{kw.phrase}</td>
<td style={{ textAlign: 'right' }}>{formatNumber(kw.searchVolume)}</td>
<td style={{ textAlign: 'right' }}>{kw.keywordDifficulty}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Top 10 Referring Domains */}
{selectedDomain.top10ReferringDomains && selectedDomain.top10ReferringDomains.length > 0 && (
<div className="modal-section">
<h4>
🔗 Top 10 Referring Domains
</h4>
<div className="modal-table-wrapper">
<table className="modal-table">
<thead>
<tr>
<th style={{ textAlign: 'left' }}>Domain</th>
<th style={{ textAlign: 'right' }}>Authority Score</th>
<th style={{ textAlign: 'right' }}>Backlinks</th>
</tr>
</thead>
<tbody>
{selectedDomain.top10ReferringDomains.map((ref, idx) => (
<tr key={idx}>
<td style={{ fontWeight: '500' }}>{ref.domain}</td>
<td style={{ textAlign: 'right' }}>{ref.domainAscore}</td>
<td style={{ textAlign: 'right' }}>{formatNumber(ref.backlinksNum)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
)
}
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

22
src/index.css Normal file
View File

@ -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;
}

11
src/main.jsx Normal file
View File

@ -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(
<React.StrictMode>
<App />
</React.StrictMode>,
)

1344
src/services/semrushApi.js Normal file

File diff suppressed because it is too large Load Diff

46
vite.config.js Normal file
View File

@ -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';
});
}
}
}
}
})