feat: Adsterra integration, code splitting, cleanup, Onidel affiliate

This commit is contained in:
dwindown
2026-02-18 16:50:45 +07:00
parent 7ba289be5c
commit 9dc3285adb
40 changed files with 352 additions and 2933 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
REACT_APP_GA_ID=G-S3K5P2PWV6

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
# Google Analytics Measurement ID
REACT_APP_GA_ID=G-S3K5P2PWV6

View File

@@ -1,436 +0,0 @@
# AdSense Strategy - Revised (Tool Pages Only)
## Strategy: Clean Homepage + Monetized Tool Pages
### ✅ Why This is BETTER:
1. **Better First Impression**: Clean homepage attracts users without ad clutter
2. **Higher Engagement**: Users explore tools without distraction
3. **Better CTR**: Ads on tool pages have higher relevance (users are actively working)
4. **Lower Bounce Rate**: No ads on homepage = users stay longer
5. **SEO Benefits**: Clean homepage ranks better
6. **Professional Image**: Looks more trustworthy and premium
---
## Ad Placement (Tool Pages Only)
### Desktop Layout (From PROJECT_ROADMAP.md)
```
┌────────────────────────────┬─────────┐
│ │ [Ad] │ ← 300x250
│ Main Content │ │
│ (Tool Editor) │ 300px │
│ │ │
│ │ [Ad] │ ← 300x250
│ │ │
│ │ [Ad] │ ← 300x250
└────────────────────────────┴─────────┘
```
**Specifications:**
- **Right Sidebar**: 300px fixed width
- **Sticky Scroll**: Ads stay visible while scrolling
- **3 Ad Blocks Maximum**:
- Ad 1: 300x250 (Medium Rectangle)
- Ad 2: 300x250 (Medium Rectangle)
- Ad 3: 300x250 (Medium Rectangle)
- **Google AdSense Compliance**: All ads fully viewable, no scrollable containers
- **Responsive**: Hide below 1200px viewport width
- **Main Content**: `calc(100% - 320px)` width
### Mobile Layout
```
┌─────────────────────────┐
│ │
│ Main Content │
│ (Scrollable) │
│ │
├─────────────────────────┤
│ [Ad Banner 320x50] │ ← Sticky Bottom
└─────────────────────────┘
```
**Specifications:**
- **Sticky Bottom Banner**: 320x50 or 320x100
- **Close Button**: Better UX
- **Content Padding**: Add padding-bottom to prevent overlap
---
## Revenue Estimation
### Traffic Assumptions
**Current Tools:** 11 tools
- Object Editor
- Table Editor
- Invoice Editor
- Text Length Tool
- Base64 Encoder/Decoder
- URL Encoder/Decoder
- Hash Generator
- JWT Decoder
- Timestamp Converter
- Color Converter
- UUID Generator
**Traffic Breakdown:**
- **Homepage**: 40% of traffic (no ads)
- **Tool Pages**: 60% of traffic (monetized)
**Monthly Traffic Estimate:**
- Total visitors: 10,000/month (conservative start)
- Homepage visits: 10,000 (entry point)
- Tool page visits: 15,000 (1.5 pages per user)
- **Monetized page views**: 15,000/month
### Ad Performance Metrics
**Desktop (70% of traffic):**
- 10,500 page views/month
- 3 ads per page = 31,500 ad impressions
- Average CPM: $3.00 (developer tools niche)
- **Revenue**: 31,500 × $3.00 / 1000 = **$94.50/month**
**Mobile (30% of traffic):**
- 4,500 page views/month
- 1 ad per page = 4,500 ad impressions
- Average CPM: $2.00 (mobile typically lower)
- **Revenue**: 4,500 × $2.00 / 1000 = **$9.00/month**
**Total Monthly Revenue (Conservative):**
```
Desktop: $94.50
Mobile: $9.00
─────────────────
TOTAL: $103.50/month
```
---
## Scaled Revenue Projections
### Scenario 1: Moderate Growth (3 months)
**Traffic:** 30,000 visitors/month
- Tool page views: 45,000/month
- Desktop impressions: 94,500
- Mobile impressions: 13,500
- **Monthly Revenue**: $310/month
### Scenario 2: Good Growth (6 months)
**Traffic:** 50,000 visitors/month
- Tool page views: 75,000/month
- Desktop impressions: 157,500
- Mobile impressions: 22,500
- **Monthly Revenue**: $517/month
### Scenario 3: Strong Growth (12 months)
**Traffic:** 100,000 visitors/month
- Tool page views: 150,000/month
- Desktop impressions: 315,000
- Mobile impressions: 45,000
- **Monthly Revenue**: $1,035/month
### Scenario 4: Viral Success (18+ months)
**Traffic:** 250,000 visitors/month
- Tool page views: 375,000/month
- Desktop impressions: 787,500
- Mobile impressions: 112,500
- **Monthly Revenue**: $2,587/month
---
## Comparison: Homepage Ads vs No Homepage Ads
### With Homepage Ads (Your ADSENSE_STRATEGY.md)
```
Homepage: 2 ads × 10,000 views = 20,000 impressions
Tool Pages: 2 ads × 15,000 views = 30,000 impressions
─────────────────────────────────────────────────────
Total: 50,000 impressions
Revenue: $150/month (at $3 CPM)
Pros: Higher revenue (+$46.50/month)
Cons: Cluttered homepage, higher bounce rate, worse SEO
```
### Without Homepage Ads (Revised Strategy)
```
Homepage: 0 ads × 10,000 views = 0 impressions
Tool Pages: 3 ads × 15,000 views = 45,000 impressions
─────────────────────────────────────────────────────
Total: 45,000 impressions
Revenue: $103.50/month (at $3 CPM)
Pros: Clean homepage, better UX, better SEO, higher retention
Cons: Lower initial revenue (-$46.50/month)
```
### Long-Term Impact
**With Clean Homepage:**
- Better SEO → More organic traffic → More tool page views
- Lower bounce rate → More pages per session → More ad impressions
- Professional image → More return visitors → Higher lifetime value
**Estimated Long-Term Benefit:**
- 20-30% more traffic from better SEO
- 15-20% more pages per session
- **Net result**: Clean homepage strategy wins after 3-6 months
---
## Implementation Plan
### Phase 1: Ad Space Preparation (1 day)
**Create Components:**
```javascript
// src/components/AdColumn.jsx
import React from 'react';
import AdBlock from './AdBlock';
const AdColumn = () => {
return (
<aside className="hidden xl:block w-[300px] ml-5 flex-shrink-0">
<div className="fixed top-20 right-8 w-[300px] space-y-5">
<AdBlock slot="1234567890" size="300x250" />
<AdBlock slot="0987654321" size="300x250" />
<AdBlock slot="1122334455" size="300x250" />
</div>
</aside>
);
};
export default AdColumn;
```
```javascript
// src/components/AdBlock.jsx
import React, { useEffect } from 'react';
const AdBlock = ({ slot, size }) => {
useEffect(() => {
try {
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}, []);
const [width, height] = size.split('x');
return (
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden">
<ins
className="adsbygoogle"
style={{ display: 'block', width: `${width}px`, height: `${height}px` }}
data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"
data-ad-slot={slot}
/>
</div>
);
};
export default AdBlock;
```
```javascript
// src/components/MobileAdBanner.jsx
import React, { useEffect, useState } from 'react';
import { X } from 'lucide-react';
const MobileAdBanner = () => {
const [visible, setVisible] = useState(true);
useEffect(() => {
try {
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}, []);
if (!visible) return null;
return (
<div className="xl:hidden fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 shadow-lg">
<button
onClick={() => setVisible(false)}
className="absolute top-1 right-1 p-1 text-gray-500 hover:text-gray-700"
>
<X className="h-4 w-4" />
</button>
<div className="flex justify-center py-2">
<ins
className="adsbygoogle"
style={{ display: 'inline-block', width: '320px', height: '50px' }}
data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"
data-ad-slot="5544332211"
/>
</div>
</div>
);
};
export default MobileAdBanner;
```
**Update ToolLayout:**
```javascript
// src/components/ToolLayout.jsx
import AdColumn from './AdColumn';
import MobileAdBanner from './MobileAdBanner';
const ToolLayout = ({ children }) => {
return (
<div className="flex gap-5 max-w-[1400px] mx-auto px-4">
{/* Main Content */}
<main className="flex-1 min-w-0">
{children}
</main>
{/* Desktop Ad Column */}
<AdColumn />
{/* Mobile Ad Banner */}
<MobileAdBanner />
</div>
);
};
```
### Phase 2: AdSense Integration (1 day)
1. **Apply for AdSense** (if not done)
2. **Add script to index.html**:
```html
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXXXXXXXX"
crossorigin="anonymous"></script>
```
3. **Create ad units** in AdSense dashboard
4. **Replace placeholder IDs** in components
5. **Test on all tools**
### Phase 3: Testing & Optimization (ongoing)
- Monitor ad viewability
- Test different ad positions
- A/B test ad sizes
- Track CTR and RPM
- Optimize based on data
---
## Success Metrics
### Month 1 (Launch)
- [ ] Ads live on all 11 tools
- [ ] Ad impressions: 45,000+
- [ ] Revenue: $100+
- [ ] No performance issues
- [ ] No user complaints
### Month 3 (Optimization)
- [ ] Traffic: 30,000 visitors/month
- [ ] Ad impressions: 135,000+
- [ ] Revenue: $300+
- [ ] Optimized ad positions
- [ ] Improved CTR
### Month 6 (Growth)
- [ ] Traffic: 50,000 visitors/month
- [ ] Ad impressions: 225,000+
- [ ] Revenue: $500+
- [ ] Consider PRO tier
### Month 12 (Maturity)
- [ ] Traffic: 100,000 visitors/month
- [ ] Ad impressions: 450,000+
- [ ] Revenue: $1,000+
- [ ] PRO tier launched
---
## PRO Tier Strategy
### When to Launch PRO:
- After 3-6 months of ad revenue
- When traffic is 50,000+/month
- When users request ad-free option
- When revenue is stable
### PRO Benefits:
-**Ad-Free Experience** - No ads anywhere
-**Backend Proxy** - CORS bypass for any API
-**Saved Work** - Cloud storage for projects
-**Shareable Links** - Share work with team
-**Priority Support** - Email support
-**Export Templates** - Save and reuse configurations
### Pricing:
- **1 Month**: $2.99
- **3 Months**: $6.99 (save 22%)
- **6 Months**: $11.99 (save 33%)
- **12 Months**: $19.99 (save 44%)
### Revenue Mix (After PRO Launch):
```
Ad Revenue: $800/month (80% of users)
PRO Revenue: $400/month (20 users × $20/year ÷ 12)
──────────────────────────────────────
Total: $1,200/month
```
---
## Summary
### Current Strategy (Revised):
**Clean Homepage** - No ads, better UX, better SEO
**Tool Pages Only** - 3 ads on desktop, 1 on mobile
**Conservative Start** - $100/month with 10K visitors
**Growth Potential** - $1,000+/month with 100K visitors
**PRO Tier Later** - Additional revenue stream
### Revenue Timeline:
```
Month 1: $100 (10K visitors)
Month 3: $300 (30K visitors)
Month 6: $500 (50K visitors)
Month 12: $1,000 (100K visitors)
Month 18: $1,500 (100K visitors + PRO tier)
```
### Why This Works:
1. Clean homepage attracts more users
2. Better SEO = more organic traffic
3. Higher engagement = more tool page views
4. More tool page views = more ad impressions
5. Professional image = higher trust = more return visits
**This strategy prioritizes long-term growth over short-term revenue!** 🚀
Step 1: Create Ad Units in AdSense Dashboard
Go to your AdSense dashboard and create these ad units:
Desktop Ads (3 units):
1. Name: "Tool Sidebar 1"
- Size: 300x250 (Medium Rectangle)
- Type: Display ads
2. Name: "Tool Sidebar 2"
- Size: 300x250 (Medium Rectangle)
- Type: Display ads
3. Name: "Tool Sidebar 3"
- Size: 300x250 (Medium Rectangle)
- Type: Display ads
Mobile Ad (1 unit):
1. Name: "Mobile Bottom Banner"
- Size: 320x50 (Mobile Banner)
- Type: Display ads
After creating each unit, you'll get an Ad Slot ID like 1234567890. Copy those IDs and give them to me.

View File

@@ -1,252 +0,0 @@
# AdSense Setup Guide - Final Steps
## ✅ What's Already Done:
1.**AdSense Script Added** to `public/index.html`
2.**AdBlock Component** created (`src/components/AdBlock.js`)
3.**AdColumn Component** created (`src/components/AdColumn.js`)
4.**MobileAdBanner Component** created (`src/components/MobileAdBanner.js`)
5.**ToolLayout Updated** to include ads on all tool pages
6.**Build Successful** - Ready to deploy!
---
## 🎯 What You Need to Do Now:
### Step 1: Create Ad Units in AdSense Dashboard
Go to: https://adsense.google.com/
**Navigate to:** Ads → By ad unit → Display ads
**Create 4 Ad Units:**
#### **Ad Unit 1: Tool Sidebar 1**
- **Name**: `Tool Sidebar 1`
- **Size**: `300x250` (Medium Rectangle)
- **Type**: Display ads
- Click "Create" and **copy the Ad Slot ID**
#### **Ad Unit 2: Tool Sidebar 2**
- **Name**: `Tool Sidebar 2`
- **Size**: `300x250` (Medium Rectangle)
- **Type**: Display ads
- Click "Create" and **copy the Ad Slot ID**
#### **Ad Unit 3: Tool Sidebar 3**
- **Name**: `Tool Sidebar 3`
- **Size**: `300x250` (Medium Rectangle)
- **Type**: Display ads
- Click "Create" and **copy the Ad Slot ID**
#### **Ad Unit 4: Mobile Bottom Banner**
- **Name**: `Mobile Bottom Banner`
- **Size**: `320x50` (Mobile Banner)
- **Type**: Display ads
- Click "Create" and **copy the Ad Slot ID**
---
### Step 2: Update Ad Slot IDs in Code
After creating the ad units, you'll have 4 slot IDs that look like: `1234567890`
**Open:** `src/components/AdColumn.js`
**Replace:**
```javascript
const AdColumn = ({
slot1 = 'REPLACE_WITH_SLOT_1', // ← Replace with your Slot 1 ID
slot2 = 'REPLACE_WITH_SLOT_2', // ← Replace with your Slot 2 ID
slot3 = 'REPLACE_WITH_SLOT_3' // ← Replace with your Slot 3 ID
}) => {
```
**With:**
```javascript
const AdColumn = ({
slot1 = '1234567890', // ← Your actual Slot 1 ID
slot2 = '0987654321', // ← Your actual Slot 2 ID
slot3 = '1122334455' // ← Your actual Slot 3 ID
}) => {
```
**Open:** `src/components/MobileAdBanner.js`
**Replace:**
```javascript
const MobileAdBanner = ({ slot = 'REPLACE_WITH_MOBILE_SLOT' }) => {
```
**With:**
```javascript
const MobileAdBanner = ({ slot = '5544332211' }) => { // ← Your Mobile Slot ID
```
---
### Step 3: Rebuild and Deploy
```bash
npm run build:no-snap
```
Then deploy to your hosting (Netlify, Vercel, etc.)
---
### Step 4: Test Ads
**After deployment:**
1. **Visit any tool page** (not homepage)
2. **Desktop**: You should see 3 ads in the right sidebar
3. **Mobile**: You should see 1 sticky banner at the bottom
4. **Homepage**: Should have NO ads (clean!)
**Note:** Ads may take 10-30 minutes to start showing after deployment.
---
## 📊 How It Works:
### **Homepage (Clean - No Ads)**
```
┌─────────────────────────────┐
│ Hero Section │
│ Tool Cards │
│ Features │
│ Footer │
└─────────────────────────────┘
```
### **Tool Pages (Desktop - 3 Ads)**
```
┌────────────────────────────┬─────────┐
│ │ [Ad1] │ 300x250
│ Tool Content │ │
│ (Object Editor, etc.) │ [Ad2] │ 300x250
│ │ │
│ │ [Ad3] │ 300x250
└────────────────────────────┴─────────┘
```
### **Tool Pages (Mobile - 1 Ad)**
```
┌─────────────────────────┐
│ Tool Content │
│ (Scrollable) │
│ │
├─────────────────────────┤
│ [Ad Banner 320x50] │ ← Sticky
└─────────────────────────┘
```
---
## 🎨 Ad Styling:
Ads are wrapped in:
- Light mode: Gray background (`bg-gray-100`)
- Dark mode: Dark gray background (`bg-gray-800`)
- Rounded corners for modern look
- Proper spacing between ads
---
## 🔧 Troubleshooting:
### **Ads Not Showing?**
1. **Wait 10-30 minutes** after deployment
2. **Check AdSense Dashboard** - Make sure account is approved
3. **Check Browser Console** for errors
4. **Disable Ad Blocker** for testing
5. **Verify Slot IDs** are correct in code
### **Ads Showing Blank Space?**
- This is normal during testing
- AdSense needs time to fill inventory
- May show blank for first few hours/days
- Will improve as site gets traffic
### **Mobile Ad Overlapping Content?**
- There's a `<div className="xl:hidden h-16" />` at bottom
- This adds padding to prevent overlap
- Adjust height if needed
---
## 📈 Monitoring Performance:
### **AdSense Dashboard:**
- Go to: https://adsense.google.com/
- Check: Reports → Overview
- Monitor:
- **Page RPM** (Revenue per 1000 impressions)
- **CTR** (Click-through rate)
- **Impressions** (How many times ads shown)
- **Earnings** (Daily/monthly revenue)
### **Expected Timeline:**
- **Day 1-7**: Low earnings, AdSense learning
- **Week 2-4**: Earnings stabilize
- **Month 2+**: Optimize based on data
---
## 🚀 Next Steps After Ads Are Live:
1. **Monitor Performance** (first week)
2. **Optimize Ad Positions** (if needed)
3. **Test Different Ad Sizes** (A/B testing)
4. **Track User Feedback** (any complaints?)
5. **Plan PRO Tier** (ad-free option)
---
## 💰 Revenue Expectations:
Based on your traffic estimate:
**Month 1:** $100-150
- 10,000 visitors
- 15,000 tool page views
- 45,000 ad impressions
**Month 3:** $300-400
- 30,000 visitors
- 45,000 tool page views
- 135,000 ad impressions
**Month 6:** $500-700
- 50,000 visitors
- 75,000 tool page views
- 225,000 ad impressions
**Month 12:** $1,000-1,500
- 100,000 visitors
- 150,000 tool page views
- 450,000 ad impressions
---
## ✅ Checklist:
- [ ] Create 4 ad units in AdSense dashboard
- [ ] Copy all 4 slot IDs
- [ ] Update `AdColumn.js` with 3 slot IDs
- [ ] Update `MobileAdBanner.js` with 1 slot ID
- [ ] Run `npm run build:no-snap`
- [ ] Deploy to production
- [ ] Test on desktop (should see 3 ads in sidebar)
- [ ] Test on mobile (should see 1 sticky banner)
- [ ] Test homepage (should see NO ads)
- [ ] Wait 30 minutes for ads to start showing
- [ ] Monitor AdSense dashboard for first earnings!
---
**You're almost done! Just need those 4 slot IDs from AdSense!** 🎉

View File

@@ -1,446 +0,0 @@
# Google AdSense Implementation Strategy
## Overview
Strategic placement of AdSense ads to monetize the developer tools while maintaining excellent user experience. Focus on non-intrusive, contextual ad placements that don't disrupt workflow.
---
## Ad Unit Types & Sizes
### 1. **Display Ads**
- **Leaderboard (728x90)**: Top of pages, above content
- **Medium Rectangle (300x250)**: Sidebar, between content sections (PRIMARY CHOICE)
- **Large Rectangle (336x280)**: Sidebar, high-visibility areas
- **Note**: For our implementation, we use 300x250 for all desktop sidebar ads to comply with Google AdSense policies (no scrollable containers)
### 2. **Responsive Ads**
- Auto-adapt to screen size
- Best for mobile compatibility
- Recommended for all placements
### 3. **In-Feed Ads**
- Native ads that blend with content
- Perfect for tool listings and results
---
## Placement Strategy
### **Homepage (High Traffic)**
**Priority: High Revenue Potential**
```
┌─────────────────────────────────────┐
│ Header / Navigation │
├─────────────────────────────────────┤
│ Hero Section │
├─────────────────────────────────────┤
│ 🟦 AD: Leaderboard (728x90) │ ← Ad #1
│ Above tool cards │
├─────────────────────────────────────┤
│ Tool Cards Grid │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Tool 1│ │Tool 2│ │Tool 3│ │
│ └──────┘ └──────┘ └──────┘ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Tool 4│ │Tool 5│ │Tool 6│ │
│ └──────┘ └──────┘ └──────┘ │
├─────────────────────────────────────┤
│ 🟦 AD: Medium Rectangle (300x250) │ ← Ad #2
│ Between tool sections │
├─────────────────────────────────────┤
│ More Tool Cards │
├─────────────────────────────────────┤
│ Features Section │
├─────────────────────────────────────┤
│ Footer │
└─────────────────────────────────────┘
```
**Ad Placements:**
- **Ad #1**: Leaderboard above tool cards (high visibility)
- **Ad #2**: Medium Rectangle between tool sections (natural break)
---
### **Tool Pages (Main Revenue Source)**
**Priority: Balanced UX + Revenue**
```
┌─────────────────────────────────────┐
│ Header / Navigation │
├──────────────┬──────────────────────┤
│ │ │
│ Sidebar │ Main Content Area │
│ │ │
│ Tool List │ ┌────────────────┐ │
│ │ │ Input Section │ │
│ 🟦 AD Box │ └────────────────┘ │ ← Sidebar Ad
│ (300x250) │ │
│ │ 🟦 AD: Responsive │ ← Ad #1
│ │ (Between sections)│
│ │ │
│ │ ┌────────────────┐ │
│ │ │ Editor Section │ │
│ │ └────────────────┘ │
│ │ │
│ │ 🟦 AD: Responsive │ ← Ad #2
│ │ (Before export) │
│ │ │
│ │ ┌────────────────┐ │
│ │ │ Export Section │ │
│ │ └────────────────┘ │
│ │ │
└──────────────┴──────────────────────┘
```
**Ad Placements:**
- **Sidebar Ad**: Medium Rectangle (300x250) - Always visible on desktop
- **Ad #1**: Responsive ad between Input and Editor sections
- **Ad #2**: Responsive ad between Editor and Export sections
---
### **Mobile Layout**
**Priority: Non-Intrusive**
```
┌─────────────────────┐
│ Header │
├─────────────────────┤
│ Input Section │
├─────────────────────┤
│ 🟦 AD: Responsive │ ← Ad #1 (Anchor/Banner)
├─────────────────────┤
│ Editor Section │
├─────────────────────┤
│ 🟦 AD: Responsive │ ← Ad #2 (Between sections)
├─────────────────────┤
│ Export Section │
├─────────────────────┤
│ Footer │
└─────────────────────┘
```
**Mobile-Specific:**
- Use **Anchor Ads** (sticky bottom banner)
- Responsive ads that adapt to screen width
- Fewer ads to maintain UX
---
## Specific Tool Placements
### **Table Editor**
```
Input Section (URL/Paste/Open)
🟦 AD: Responsive (320x100 mobile, 728x90 desktop)
Table Editor (Main workspace)
🟦 AD: Medium Rectangle (300x250) - Right aligned
Export Section
```
### **Object Editor**
```
Input Section
🟦 AD: Responsive
Visual Editor / Mindmap / Table View
🟦 AD: Responsive (before export)
Export Results
```
### **Invoice Editor**
```
Invoice Form
🟦 AD: Sidebar (300x250) - Desktop only
Preview Section
🟦 AD: Responsive (before export)
Export Options
```
### **Converter/Formatter Tools**
```
Input Textarea
🟦 AD: Responsive
Convert/Format Button
Output Textarea
🟦 AD: Medium Rectangle (if space allows)
```
---
## Ad Frequency Rules
### **Maximum Ads Per Page:**
- **Homepage**: 2-3 ads
- **Tool Pages**: 2-3 ads (desktop), 1-2 ads (mobile)
- **Never**: More than 1 ad per viewport height
### **Minimum Content-to-Ad Ratio:**
- At least 300px of content between ads
- Never place ads immediately adjacent
- Maintain 50% content, 50% white space, minimal ads
---
## PRO User Benefits (Ad-Free)
### **Free Users:**
- See all ads as described above
- Full tool functionality
- Standard experience
### **PRO Users ($5-10/month):**
-**Ad-Free Interface** - No ads anywhere
-**Backend Proxy** - CORS bypass for any API
-**Saved Work** - Cloud storage for projects
-**Shareable Links** - Share work with team
-**Advanced Features** - Custom HTTP methods, headers, auth
-**Priority Support** - Email support
-**Export Templates** - Save and reuse configurations
---
## Implementation Plan
### **Phase 1: Basic AdSense Setup (Week 1)**
- [ ] Apply for Google AdSense account
- [ ] Get approval (usually 1-2 weeks)
- [ ] Create ad units in AdSense dashboard
- [ ] Get ad unit codes
### **Phase 2: Homepage Integration (Week 2)**
- [ ] Create `AdSense` component
- [ ] Add Leaderboard ad above tool cards
- [ ] Add Medium Rectangle between sections
- [ ] Test responsive behavior
- [ ] Verify ad display and tracking
### **Phase 3: Tool Pages Integration (Week 3)**
- [ ] Add sidebar ad component
- [ ] Add responsive ads between sections
- [ ] Implement mobile anchor ads
- [ ] Test on all tools
- [ ] Optimize placement based on CTR
### **Phase 4: PRO Feature Integration (Week 4)**
- [ ] Create PRO user detection system
- [ ] Hide ads for PRO users
- [ ] Add "Remove Ads" upgrade prompt
- [ ] Implement payment system (Stripe)
- [ ] Test PRO vs FREE experience
---
## Technical Implementation
### **AdSense Component**
```javascript
// src/components/AdSense.js
import React, { useEffect } from 'react';
import { getCurrentUserTier, USER_TIER } from '../config/features';
const AdSense = ({
slot,
format = 'auto',
responsive = true,
style = {}
}) => {
const userTier = getCurrentUserTier();
// Don't show ads for PRO users
if (userTier === USER_TIER.PRO) {
return null;
}
useEffect(() => {
try {
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}, []);
return (
<div className="adsense-container my-4">
<ins
className="adsbygoogle"
style={{ display: 'block', ...style }}
data-ad-client="ca-pub-XXXXXXXXXXXXXXXX" // Your AdSense ID
data-ad-slot={slot}
data-ad-format={format}
data-full-width-responsive={responsive}
/>
</div>
);
};
export default AdSense;
```
### **Usage Example**
```javascript
// In TableEditor.js
import AdSense from '../components/AdSense';
// Between sections
<AdSense
slot="1234567890"
format="auto"
responsive={true}
/>
// Sidebar
<AdSense
slot="0987654321"
format="rectangle"
style={{ width: '300px', height: '250px' }}
/>
```
### **Add Script to index.html**
```html
<!-- public/index.html -->
<head>
<!-- Google AdSense -->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXXXXXXXX"
crossorigin="anonymous"></script>
</head>
```
---
## Revenue Estimation
### **Traffic Assumptions:**
- 1,000 daily visitors
- 3 page views per visitor = 3,000 page views/day
- 90,000 page views/month
### **AdSense Metrics:**
- Average CPM: $2-5 (developer tools niche)
- Average CTR: 1-2%
- Average CPC: $0.50-2.00
### **Monthly Revenue Estimate:**
**Conservative (Low End):**
- 90,000 page views × $2 CPM = $180/month
- Or: 90,000 × 1% CTR × $0.50 CPC = $450/month
**Optimistic (High End):**
- 90,000 page views × $5 CPM = $450/month
- Or: 90,000 × 2% CTR × $2.00 CPC = $3,600/month
**Realistic Target:** $300-800/month with optimization
---
## Best Practices
### **Do's:**
✅ Place ads in natural content breaks
✅ Use responsive ad units
✅ Test different placements and track CTR
✅ Maintain good content-to-ad ratio
✅ Respect user experience
✅ Offer ad-free PRO option
### **Don'ts:**
❌ Place ads in middle of forms or editors
❌ Use too many ads per page
❌ Hide ads with CSS (against policy)
❌ Click own ads (instant ban)
❌ Encourage clicks ("Click here!")
❌ Place ads too close together
---
## Monitoring & Optimization
### **Key Metrics to Track:**
1. **Page RPM** (Revenue per 1000 impressions)
2. **CTR** (Click-through rate)
3. **CPC** (Cost per click)
4. **Viewability** (% of ads actually seen)
5. **User Engagement** (bounce rate, time on site)
### **A/B Testing:**
- Test different ad positions
- Test ad sizes and formats
- Monitor which tools generate most revenue
- Optimize based on data
### **Monthly Review:**
- Analyze AdSense reports
- Identify top-performing placements
- Remove low-performing ads
- Test new positions
---
## Compliance & Policy
### **Google AdSense Policies:**
- No invalid clicks or impressions
- No prohibited content
- Proper ad placement (not deceptive)
- Privacy policy must mention ads
- Cookie consent for EU users (already implemented)
### **Privacy Policy Update:**
```markdown
## Advertising
We use Google AdSense to display advertisements on our website.
Google AdSense uses cookies to serve ads based on your prior visits
to our website or other websites. You may opt out of personalized
advertising by visiting Google's Ads Settings.
Third-party vendors, including Google, use cookies to serve ads
based on a user's prior visits to our website. Users may opt out
of Google's use of cookies by visiting the Google advertising
opt-out page.
```
---
## Summary
### **Immediate Actions:**
1. ✅ Hide Advanced Options (Done)
2. 📝 Apply for Google AdSense
3. 📝 Create AdSense component
4. 📝 Implement homepage ads first
5. 📝 Test and optimize
### **Future Enhancements:**
- PRO subscription system
- Backend proxy for CORS
- Saved work and templates
- Team collaboration features
- Analytics dashboard
**Goal:** Generate $500-1000/month from ads while maintaining excellent UX, then offer PRO tier for ad-free experience + premium features.

201
README.md
View File

@@ -1,116 +1,151 @@
# Web Developer Tools MVP
# Dewe.Dev - Developer Tools
A modern, responsive web application containing essential utility tools for web developers and programmers. Built with React and TailwindCSS for a clean, fast, and user-friendly experience.
A professional, responsive web application containing essential utility tools for web developers and programmers. Built with React and TailwindCSS for a fast, clean, and user-friendly experience.
## 🚀 Features
### Available Tools
1. **JSON Encoder/Decoder** - Format, validate, and minify JSON data with syntax highlighting
2. **Serialize Encoder/Decoder** - Encode and decode PHP serialized data
3. **URL Encoder/Decoder** - Encode and decode URLs and query parameters
4. **Base64 Encoder/Decoder** - Convert text to Base64 and back with file support
5. **CSV ↔ JSON Converter** - Convert between CSV and JSON formats with custom delimiters
6. **Code Beautifier/Minifier** - Format and minify JSON, XML, SQL, CSS, and HTML code
7. **Text Diff Checker** - Compare two texts and highlight differences line by line
1. **Object Editor** (`/object-editor`) — Visual editor for JSON and PHP serialized objects with mindmap visualization.
2. **Table Editor** (`/table-editor`) — Import, edit, and export tabular data from URLs, files, or paste CSV/JSON.
3. **Markdown Editor** (`/markdown-editor`) — Write and preview markdown with live rendering, syntax highlighting, and export.
4. **Invoice Editor** (`/invoice-editor`) — Create, edit, and export professional invoices with PDF generation.
5. **URL Encoder/Decoder** (`/url`) — Encode and decode URLs and query parameters.
6. **Base64 Encoder/Decoder** (`/base64`) — Convert text to Base64 and back with file support.
7. **Code Beautifier/Minifier** (`/beautifier`) — Format and minify JSON, XML, SQL, CSS, and HTML code.
8. **Text Diff Checker** (`/diff`) — Compare two texts and highlight differences line by line.
9. **Text Length Checker** (`/text-length`) — Analyze text length, word count, and other text statistics.
### Key Features
-**Modern UI** - Clean, minimalist design with automatic dark/light mode
- 📱 **Fully Responsive** - Equal experience on desktop and mobile devices
-**Lightning Fast** - All processing happens locally in the browser
- 🔒 **Privacy First** - No server dependencies, no data collection
- 📋 **Easy Copy-Paste** - One-click copy functionality for all outputs
- 📁 **File Support** - Upload files directly for processing
- 🔍 **Searchable** - Quick search through available tools
-**Modern UI** Clean, minimalist design with automatic dark/light mode.
- 📱 **Fully Responsive** Equal experience on desktop and mobile devices.
-**Lightning Fast** — Code-splitting with React.lazy for optimal loading performance.
- 🔒 **Privacy First** No server dependencies, no data collection.
- 📋 **Easy Copy-Paste** One-click copy functionality for all outputs.
- 📁 **File Support** Upload files directly for processing.
- 🔍 **Searchable** Quick search through available tools.
- 🎯 **SEO Optimized** — Pre-rendered pages with React Snap, meta tags, and sitemap.
## 🛠 Tech Stack
- **Frontend**: React 18, React Router
- **Styling**: TailwindCSS with custom design system
- **Frontend**: React 18, React Router 6
- **Styling**: TailwindCSS 3
- **Editor**: CodeMirror 6 (@uiw/react-codemirror)
- **PDF Generation**: jsPDF, jsPDF-AutoTable
- **Markdown**: marked, DOMPurify, highlight.js
- **Icons**: Lucide React
- **Build Tool**: Create React App
- **Libraries**:
- js-beautify (code formatting)
- **SEO**: react-helmet-async
- **Analytics**: Google Analytics 4 with Consent Mode v2
- **Advertising**: Adsterra
- **Utilities**:
- papaparse (CSV parsing)
- js-beautify (code formatting)
- serialize-javascript (serialization)
- diff-match-patch (text comparison)
- turndown (HTML to Markdown)
## 📦 Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd developer-tools
```
```bash
git clone <repository-url>
cd developer-tools
```
2. Install dependencies:
```bash
npm install
```
```bash
npm install
```
3. Start the development server:
```bash
npm start
```
3. Configure environment (optional):
```bash
cp .env.example .env
# Edit .env with your GA measurement ID
```
4. Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
4. Start the development server:
```bash
npm run dev
# or
npm start
```
5. Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
## 🏗 Project Structure
```
src/
├── components/ # Reusable UI components
│ ├── Layout.js # Main layout wrapper
│ ├── ToolCard.js # Tool card component for home page
│ ├── ToolLayout.js # Layout for individual tools
── CopyButton.js # Copy to clipboard button
├── pages/ # Individual tool pages
│ ├── Home.js # Homepage with tool listing
│ ├── JsonTool.js # JSON encoder/decoder
│ ├── SerializeTool.js # Serialize encoder/decoder
│ ├── UrlTool.js # URL encoder/decoder
│ ├── Base64Tool.js # Base64 encoder/decoder
│ ├── CsvJsonTool.js # CSV/JSON converter
│ ├── BeautifierTool.js# Code beautifier/minifier
── DiffTool.js # Text diff checker
├── App.js # Main app component with routing
├── index.js # React app entry point
└── index.css # Global styles and Tailwind imports
├── components/ # Reusable UI components
│ ├── Layout.js # Main layout with sidebar navigation
│ ├── ToolLayout.js # Wrapper for tool pages with ad slots
│ ├── ToolCard.js # Tool card for homepage
── ToolSidebar.js # Sidebar navigation
│ ├── AdBlock.js # Adsterra ad unit wrapper
│ ├── AdColumn.js # Desktop sidebar ad column
│ ├── MobileAdBanner.js # Mobile bottom ad banner
│ ├── OfferBlock.js # Promotional offer slot
│ ├── AffiliateBlock.js # Affiliate link slot
│ ├── Loading.js # Loading spinner for Suspense
│ ├── CodeMirrorEditor.js # Code editor component
│ ├── CodeEditor.js # Alternative code editor
── SEO.js / SEOHead.js # SEO meta tags
│ ├── RelatedTools.js # Related tools suggestions
│ ├── ErrorBoundary.js # Error boundary
│ ├── ThemeToggle.js # Dark/light mode toggle
│ ├── CopyButton.js # One-click copy
│ ├── ConsentBanner.js # GDPR consent banner
│ ├── MindmapView.js # JSON visualization
│ ├── StructuredEditor.js # Object editor modal
│ └── ProBadge.js # PRO feature badge
├── pages/ # Tool pages (lazy-loaded)
│ ├── Home.js
│ ├── ObjectEditor.js
│ ├── TableEditor.js
│ ├── MarkdownEditor.js
│ ├── InvoiceEditor.js
│ ├── InvoicePreview.js
│ ├── InvoicePreviewMinimal.js
│ ├── BeautifierTool.js
│ ├── UrlTool.js
│ ├── Base64Tool.js
│ ├── DiffTool.js
│ ├── TextLengthTool.js
│ ├── ReleaseNotes.js
│ ├── PrivacyPolicy.js
│ ├── TermsOfService.js
│ └── NotFound.js
├── config/ # App configuration
│ ├── tools.js # Tool definitions (single source of truth)
│ └── features.js # Feature flags (FREE/PRO tiers)
├── utils/ # Utility functions
│ ├── analytics.js # Google Analytics 4
│ ├── consentManager.js # GDPR consent management
│ ├── seo.js # SEO helpers
│ ├── browserCompat.js # Browser compatibility
│ ├── contentExtractor.js # Content parsing
│ └── sitemapGenerator.js # Sitemap utilities
└── hooks/ # Custom React hooks
├── useAnalytics.js # Analytics hook
└── useNavigationGuard.js # Unsaved changes protection
```
## 🎨 Design System
The application uses a consistent design system built with TailwindCSS:
- **Colors**: Primary blue theme with automatic dark/light mode
- **Typography**: System fonts with JetBrains Mono for code
- **Components**: Reusable utility classes for consistent styling
- **Responsive**: Mobile-first design approach
## 🚀 Available Scripts
- `npm start` - Runs the app in development mode
- `npm build` - Builds the app for production
- `npm test` - Launches the test runner
- `npm run eject` - Ejects from Create React App (one-way operation)
- `npm run dev` or `npm start` — Runs the app in development mode.
- `npm run build` — Builds the app for production (with React Snap for pre-rendering).
- `npm run build:no-snap` — Builds the app for production (without pre-rendering).
- `npm test` — Launches the test runner.
- `npm run eject` — Ejects from Create React App (one-way operation).
## 📱 Usage
## 📁 Environment Variables
1. **Home Page**: Browse and search through available tools
2. **Individual Tools**: Each tool has a dedicated page with:
- Input area for your data
- Processing controls (encode/decode, beautify/minify, etc.)
- Output area with copy functionality
- File upload support where applicable
- Usage tips and examples
| Variable | Description | Default |
|----------|-------------|---------|
| `REACT_APP_GA_ID` | Google Analytics Measurement ID | `G-S3K5P2PWV6` |
## 🔧 Customization
The application is built with scalability in mind:
- **Adding New Tools**: Create a new page component and add it to the routing
- **Styling**: Modify `tailwind.config.js` for theme customization
- **Components**: All components are modular and reusable
Create a `.env` file based on `.env.example` to override defaults.
## 🌟 Contributing
@@ -123,13 +158,3 @@ The application is built with scalability in mind:
## 📄 License
This project is open source and available under the MIT License.
## 🎯 Roadmap
Future enhancements could include:
- More encoding/decoding formats
- Advanced diff algorithms
- Syntax highlighting for code
- Export functionality
- Keyboard shortcuts
- Tool favorites/bookmarks

View File

@@ -19,12 +19,10 @@
### 1. **Add Missing Tools to Sitemap**
**Current Issue:** Sitemap is missing several tools:
- ❌ Markdown Editor
- ❌ JSON Tool
- ❌ CSV/JSON Converter
- ❌ Serialize Tool
- ❌ Release Notes page
**Current Issue:** Sitemap was missing the Markdown Editor and Release Notes pages.
> **Note:** `/json`, `/csv-json`, and `/serialize` routes were planned but never implemented.
> These tools are now consolidated into the Object Editor (`/object-editor`).
**Action:** Update `public/sitemap.xml`
@@ -36,24 +34,6 @@
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dewe.dev/json</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/csv-json</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/serialize</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/release-notes</loc>
<lastmod>2025-10-22</lastmod>
@@ -296,8 +276,8 @@ Write and preview markdown with live rendering
**Add to `index.html`:**
```html
<link rel="preconnect" href="https://pagead2.googlesyndication.com">
<link rel="dns-prefetch" href="https://pagead2.googlesyndication.com">
<link rel="preconnect" href="https://bustleplaguereed.com">
<link rel="dns-prefetch" href="https://bustleplaguereed.com">
```
---

1
package-lock.json generated
View File

@@ -45,7 +45,6 @@
"react-dom": "18.3.1",
"react-helmet-async": "^2.0.5",
"react-router-dom": "6.26.2",
"react-scripts": "5.0.1",
"react-snap": "^1.23.0",
"reactflow": "^11.11.4",
"serialize-javascript": "^6.0.0",

View File

@@ -4,7 +4,6 @@
"description": "Web Developer Tools MVP - Utilities Toolkit",
"private": true,
"dependencies": {
"@codemirror/basic-setup": "^0.20.0",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
@@ -41,7 +40,6 @@
"react-dom": "18.3.1",
"react-helmet-async": "^2.0.5",
"react-router-dom": "6.26.2",
"react-scripts": "5.0.1",
"react-snap": "^1.23.0",
"reactflow": "^11.11.4",
"serialize-javascript": "^6.0.0",
@@ -56,6 +54,7 @@
"tailwindcss": "^3.3.0"
},
"scripts": {
"dev": "react-scripts start",
"start": "react-scripts start",
"start:prod": "serve -s build -l 3000",
"build": "react-scripts build && react-snap",
@@ -83,7 +82,7 @@
"/beautifier",
"/diff",
"/text-length",
"/whats-new",
"/markdown-editor",
"/privacy",
"/terms"
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -14,9 +14,7 @@
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Developer Tools - Web Developer Utilities</title>
<!-- Google AdSense -->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8644544686212757"
crossorigin="anonymous"></script>
<!-- Adsterra -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -6,6 +6,18 @@
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"start_url": ".",

View File

@@ -72,12 +72,6 @@
</url>
<!-- Info Pages -->
<url>
<loc>https://dewe.dev/whats-new</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://dewe.dev/release-notes</loc>
<lastmod>2025-10-22</lastmod>

View File

@@ -1,28 +1,30 @@
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import React, { useEffect, Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import Layout from './components/Layout';
import ErrorBoundary from './components/ErrorBoundary';
import Home from './pages/Home';
import UrlTool from './pages/UrlTool';
import Base64Tool from './pages/Base64Tool';
import BeautifierTool from './pages/BeautifierTool';
import DiffTool from './pages/DiffTool';
import TextLengthTool from './pages/TextLengthTool';
import ObjectEditor from './pages/ObjectEditor';
import TableEditor from './pages/TableEditor';
import InvoiceEditor from './pages/InvoiceEditor';
import MarkdownEditor from './pages/MarkdownEditor';
import InvoicePreview from './pages/InvoicePreview';
import InvoicePreviewMinimal from './pages/InvoicePreviewMinimal';
import ReleaseNotes from './pages/ReleaseNotes';
import TermsOfService from './pages/TermsOfService';
import PrivacyPolicy from './pages/PrivacyPolicy';
import NotFound from './pages/NotFound';
import Loading from './components/Loading';
import { initGA } from './utils/analytics';
import './index.css';
const Home = lazy(() => import('./pages/Home'));
const UrlTool = lazy(() => import('./pages/UrlTool'));
const Base64Tool = lazy(() => import('./pages/Base64Tool'));
const BeautifierTool = lazy(() => import('./pages/BeautifierTool'));
const DiffTool = lazy(() => import('./pages/DiffTool'));
const TextLengthTool = lazy(() => import('./pages/TextLengthTool'));
const ObjectEditor = lazy(() => import('./pages/ObjectEditor'));
const TableEditor = lazy(() => import('./pages/TableEditor'));
const InvoiceEditor = lazy(() => import('./pages/InvoiceEditor'));
const MarkdownEditor = lazy(() => import('./pages/MarkdownEditor'));
const InvoicePreview = lazy(() => import('./pages/InvoicePreview'));
const InvoicePreviewMinimal = lazy(() => import('./pages/InvoicePreviewMinimal'));
const ReleaseNotes = lazy(() => import('./pages/ReleaseNotes'));
const TermsOfService = lazy(() => import('./pages/TermsOfService'));
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy'));
const NotFound = lazy(() => import('./pages/NotFound'));
function App() {
// Initialize Google Analytics on app startup
useEffect(() => {
@@ -34,6 +36,7 @@ function App() {
<ErrorBoundary>
<Router>
<Layout>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/url" element={<UrlTool />} />
@@ -47,12 +50,13 @@ function App() {
<Route path="/markdown-editor" element={<MarkdownEditor />} />
<Route path="/invoice-preview" element={<InvoicePreview />} />
<Route path="/invoice-preview-minimal" element={<InvoicePreviewMinimal />} />
<Route path="/whats-new" element={<ReleaseNotes />} />
<Route path="/whats-new" element={<Navigate to="/release-notes" replace />} />
<Route path="/release-notes" element={<ReleaseNotes />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</Layout>
</Router>
</ErrorBoundary>

View File

@@ -1,35 +1,48 @@
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
const AdBlock = ({ className = '', adKey = 'e0ca7c61c83457f093bbc2e261b43d31' }) => {
const iframeRef = useRef(null);
/**
* AdBlock Component - Individual ad unit
* Displays a single Google AdSense ad
*/
const AdBlock = ({ slot, size = '300x250', className = '' }) => {
useEffect(() => {
try {
// Push ad to AdSense queue
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}, []);
if (!iframeRef.current) return;
const [width, height] = size.split('x');
const iframe = iframeRef.current;
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; }
</style>
</head>
<body>
<script type="text/javascript">
atOptions = {
'key' : '${adKey}',
'format' : 'iframe',
'height' : 250,
'width' : 300,
'params' : {}
};
</script>
<script type="text/javascript" src="https://bustleplaguereed.com/${adKey}/invoke.js"></script>
</body>
</html>
`);
doc.close();
}, [adKey]);
return (
<div className={`bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${className}`}>
<ins
className="adsbygoogle"
style={{
display: 'block',
width: `${width}px`,
height: `${height}px`
}}
data-ad-client="ca-pub-8644544686212757"
data-ad-slot={slot}
data-ad-format="fixed"
/>
</div>
<iframe
ref={iframeRef}
className={`bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${className}`}
style={{ width: '300px', height: '250px', border: 'none' }}
title="Advertisement"
sandbox="allow-scripts allow-same-origin"
/>
);
};

View File

@@ -1,29 +1,15 @@
import React from 'react';
import AdBlock from './AdBlock';
import OfferBlock from './OfferBlock';
import AffiliateBlock from './AffiliateBlock';
/**
* AdColumn Component - Desktop sidebar with 3 ad units
* Hidden on mobile/tablet, visible on desktop (1200px+)
* All ads are 300x250 (Medium Rectangle) to comply with Google AdSense policies
* - Ads must be fully viewable without scrolling
* - No scrollable containers allowed
*/
const AdColumn = ({
slot1 = 'REPLACE_WITH_SLOT_1',
slot2 = 'REPLACE_WITH_SLOT_2',
slot3 = 'REPLACE_WITH_SLOT_3'
}) => {
const AdColumn = () => {
return (
<aside className="hidden xl:block w-[300px] flex-shrink-0">
<div className="fixed top-20 right-8 w-[300px] space-y-5">
{/* Ad 1: Medium Rectangle */}
<AdBlock slot={slot1} size="300x250" />
{/* Ad 2: Medium Rectangle */}
<AdBlock slot={slot2} size="300x250" />
{/* Ad 3: Medium Rectangle */}
<AdBlock slot={slot3} size="300x250" />
<div className="sticky top-20 space-y-5">
<AdBlock />
<OfferBlock />
<AffiliateBlock />
</div>
</aside>
);

View File

@@ -0,0 +1,21 @@
import React from 'react';
const AffiliateBlock = () => {
return (
<a
href="https://onidel.com/?referral=1568841"
target="_blank"
rel="noopener noreferrer sponsored"
className="block w-[300px] h-[250px] rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300"
>
<img
src="/images/onidel-banner.webp"
alt="Onidel - Professional Development Services"
className="w-full h-full object-cover"
loading="lazy"
/>
</a>
);
};
export default AffiliateBlock;

18
src/components/Loading.js Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Loader2 } from 'lucide-react';
const Loading = () => {
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] w-full">
<div className="relative">
<div className="absolute inset-0 bg-blue-500/20 blur-xl rounded-full animate-pulse"></div>
<Loader2 className="h-12 w-12 text-blue-600 dark:text-blue-400 animate-spin relative z-10" />
</div>
<p className="mt-4 text-slate-500 dark:text-slate-400 font-medium animate-pulse">
Loading...
</p>
</div>
);
};
export default Loading;

View File

@@ -1,17 +1,12 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { X } from 'lucide-react';
/**
* MobileAdBanner Component - Sticky bottom banner for mobile
* Visible only on mobile/tablet, hidden on desktop
* Includes close button for better UX
*/
const MobileAdBanner = ({ slot = 'REPLACE_WITH_MOBILE_SLOT' }) => {
const MobileAdBanner = () => {
const [visible, setVisible] = useState(true);
const [closed, setClosed] = useState(false);
const iframeRef = useRef(null);
useEffect(() => {
// Check if user previously closed the banner (session storage)
const wasClosed = sessionStorage.getItem('mobileAdClosed');
if (wasClosed === 'true') {
setClosed(true);
@@ -20,13 +15,41 @@ const MobileAdBanner = ({ slot = 'REPLACE_WITH_MOBILE_SLOT' }) => {
}, []);
useEffect(() => {
if (visible && !closed) {
try {
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}
if (!visible || closed || !iframeRef.current) return;
const timer = setTimeout(() => {
if (!iframeRef.current) return;
const iframe = iframeRef.current;
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; }
</style>
</head>
<body>
<script type="text/javascript">
atOptions = {
'key' : '2965bcf877388cafa84160592c550f5a',
'format' : 'iframe',
'height' : 50,
'width' : 320,
'params' : {}
};
</script>
<script type="text/javascript" src="https://bustleplaguereed.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
</body>
</html>
`);
doc.close();
}, 500);
return () => clearTimeout(timer);
}, [visible, closed]);
const handleClose = () => {
@@ -41,22 +64,17 @@ const MobileAdBanner = ({ slot = 'REPLACE_WITH_MOBILE_SLOT' }) => {
<div className="xl:hidden fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 shadow-lg border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleClose}
className="absolute top-1 right-1 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded-full shadow-sm"
className="absolute -top-2 right-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded-full shadow-sm z-10"
aria-label="Close ad"
>
<X className="h-4 w-4" />
</button>
<div className="flex justify-center py-2">
<ins
className="adsbygoogle"
style={{
display: 'inline-block',
width: '320px',
height: '50px'
}}
data-ad-client="ca-pub-8644544686212757"
data-ad-slot={slot}
data-ad-format="fixed"
<div className="flex justify-center items-center py-2">
<iframe
ref={iframeRef}
style={{ width: '320px', height: '50px', border: 'none' }}
title="Mobile Advertisement"
sandbox="allow-scripts allow-same-origin"
/>
</div>
</div>

View File

@@ -0,0 +1,22 @@
import React from 'react';
const OfferBlock = () => {
return (
<div className="w-[300px] h-[250px] bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex flex-col items-center justify-center text-white p-6 text-center shadow-lg hover:shadow-xl transition-shadow duration-300">
<span className="bg-white/20 text-xs font-bold px-2 py-1 rounded mb-4 backdrop-blur-sm">
SPECIAL OFFER
</span>
<h3 className="text-2xl font-bold mb-2">
Upgrade to PRO
</h3>
<p className="text-indigo-100 text-sm mb-6">
Get unlimited access to all developer tools and features.
</p>
<button className="bg-white text-indigo-600 font-bold py-2 px-6 rounded-full hover:bg-indigo-50 transition-colors shadow-md">
Learn More
</button>
</div>
);
};
export default OfferBlock;

View File

@@ -111,7 +111,6 @@ const InvoiceEditor = () => {
const currencyData = await response.json();
setCurrencies(currencyData);
} catch (error) {
console.error('Failed to load currencies:', error);
// Fallback to basic currencies
setCurrencies([
{ code: 'IDR', name: 'Indonesian Rupiah', symbol: 'Rp' },
@@ -165,7 +164,7 @@ const InvoiceEditor = () => {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load saved invoice:', error);
// Failed to load saved invoice
}
}, [searchParams]);
@@ -176,7 +175,7 @@ const InvoiceEditor = () => {
try {
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
} catch (error) {
console.error('Failed to save invoice:', error);
// Failed to save invoice
}
}
}, [invoiceData, createNewCompleted]);
@@ -227,7 +226,7 @@ const InvoiceEditor = () => {
try {
localStorage.setItem('pdfPageSize', pdfPageSize);
} catch (error) {
console.error('Failed to save PDF page size:', error);
// Failed to save PDF page size
}
}, [pdfPageSize]);
@@ -619,7 +618,6 @@ const InvoiceEditor = () => {
// Navigate to preview page
navigate('/invoice-preview');
} catch (error) {
console.error('Failed to save invoice data:', error);
alert('Failed to save invoice data. Please try again.');
}
};

View File

@@ -94,7 +94,6 @@ const InvoicePreview = () => {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load invoice data:', error);
navigate('/invoice-editor');
}
}, [navigate]);
@@ -262,7 +261,6 @@ const InvoicePreview = () => {
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
// Restore original styles after a short delay
@@ -281,7 +279,7 @@ const InvoicePreview = () => {
try {
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
} catch (error) {
console.error('Failed to save invoice data before edit:', error);
// Failed to save invoice data before edit
}
}
// Add a parameter to indicate we're editing existing data

View File

@@ -29,7 +29,6 @@ const InvoicePreviewMinimal = () => {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load invoice data:', error);
navigate('/invoice-editor');
}
}, [navigate]);
@@ -164,7 +163,6 @@ const InvoicePreviewMinimal = () => {
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
// Restore original styles after a short delay

View File

@@ -58,8 +58,6 @@ const MarkdownEditor = () => {
const language = token.lang || '';
const normalizedLang = language ? language.toLowerCase().trim() : '';
console.log('Code block detected:', { language, normalizedLang, codeLength: codeString.length });
let highlightedCode = codeString;
// Apply syntax highlighting
@@ -67,18 +65,14 @@ const MarkdownEditor = () => {
try {
const result = hljs.highlight(codeString, { language: normalizedLang });
highlightedCode = result.value;
console.log('Highlighted with language:', normalizedLang);
} catch (e) {
console.error(`Highlight error for ${normalizedLang}:`, e);
highlightedCode = codeString;
}
} else {
try {
const result = hljs.highlightAuto(codeString);
highlightedCode = result.value;
console.log('Auto-highlighted, detected:', result.language);
} catch (e) {
console.error('Auto-highlight error:', e);
highlightedCode = codeString;
}
}
@@ -136,7 +130,6 @@ const MarkdownEditor = () => {
ADD_ATTR: ['data-code-id', 'title', 'id', 'type', 'checked', 'disabled']
});
} catch (e) {
console.error('Markdown parse error:', e);
return '<p>Error parsing markdown</p>';
}
};
@@ -173,7 +166,7 @@ const MarkdownEditor = () => {
button.textContent = originalText;
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
// Failed to copy
});
}
return;
@@ -818,7 +811,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
response = await fetch(urlToFetch);
} catch (corsError) {
// If CORS error, try with CORS proxy
console.log('CORS error, trying with proxy...');
response = await fetch(`https://api.allorigins.win/raw?url=${encodeURIComponent(urlToFetch)}`);
}
@@ -855,7 +847,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
});
} catch (err) {
console.error('Fetch error:', err);
setError(`Failed to fetch from URL: ${err.message}`);
} finally {
setFetching(false);
@@ -1494,7 +1485,6 @@ ${html}
html2pdf().set(opt).from(wrapper).save().then(() => {
setError('');
}).catch((err) => {
console.error('PDF generation error:', err);
setError('Failed to generate PDF');
});
};

View File

@@ -385,9 +385,6 @@ const ObjectEditor = () => {
try {
const result = parseValue();
if (index < str.length) {
console.warn(`Warning: Trailing data after parsing: "${str.substring(index)}"`);
}
return result;
} catch (error) {
throw new Error(`Parse error at position ${index}: ${error.message}`);
@@ -563,7 +560,6 @@ const ObjectEditor = () => {
serialized
});
} catch (error) {
console.error('Error generating outputs:', error);
setOutputs({
jsonPretty: 'Error generating JSON',
jsonMinified: 'Error generating JSON',
@@ -610,8 +606,6 @@ const ObjectEditor = () => {
// Fetch data from URL with advanced content extraction
const handleFetchData = async (advancedOptions = null) => {
console.log('🚀 handleFetchData called with URL:', fetchUrl);
console.log('🔧 Advanced options:', advancedOptions);
const urlToFetch = advancedOptions?.url || fetchUrl.trim();
@@ -623,7 +617,6 @@ const ObjectEditor = () => {
setFetching(true);
setError('');
console.log('✅ Starting fetch process...');
try {
// Add protocol if missing
@@ -635,11 +628,10 @@ const ObjectEditor = () => {
// Determine if this is an advanced request (has custom options)
const isAdvancedRequest = advancedOptions && (
advancedOptions.method !== 'GET' ||
Object.keys(advancedOptions.headers || {}).length > 0 ||
Object.keys( advancedOptions.headers || {}).length > 0 ||
advancedOptions.body
);
console.log('🎯 Is advanced request:', isAdvancedRequest);
// Build fetch options for advanced mode
const fetchOptions = advancedOptions ? {
@@ -648,7 +640,6 @@ const ObjectEditor = () => {
body: advancedOptions.body || undefined
} : {};
console.log('📡 Fetch options:', fetchOptions);
// Try direct fetch first (for APIs)
try {
@@ -700,7 +691,6 @@ const ObjectEditor = () => {
throw e;
}
// CORS error or network error, will try with content extractor for simple GET
console.log('Direct fetch failed, trying content extractor:', e.message);
}
// Use advanced content extractor ONLY for simple GET requests to HTML pages
@@ -747,7 +737,6 @@ const ObjectEditor = () => {
});
} catch (err) {
console.error('Fetch error:', err);
setError(`Failed to fetch data: ${err.message}`);
setUrlDataSummary(null);
} finally {

View File

@@ -167,20 +167,19 @@ const PrivacyPolicy = () => {
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Globe className="h-5 w-5 text-teal-600" />
5. Future Advertising (Google AdSense)
5. Advertising (Adsterra)
</h2>
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4 mb-4">
<h3 className="font-semibold text-amber-800 dark:text-amber-200 mb-2">
🔮 Planned Implementation:
Advertising:
</h3>
<p className="text-amber-700 dark:text-amber-300 text-sm leading-relaxed mb-3">
To keep our tools free, we plan to display Google AdSense advertisements. When implemented:
To keep our tools free, we display Adsterra advertisements.
</p>
<ul className="list-disc list-inside text-amber-700 dark:text-amber-300 space-y-1 text-sm">
<li>Ads will be clearly marked and non-intrusive</li>
<li>No impact on tool functionality or performance</li>
<li>Google may use cookies for ad personalization</li>
<li>You can opt-out of personalized ads via Google settings</li>
<li>The ad network may use cookies for ad delivery</li>
<li><strong>We will NEVER share your tool usage data with advertisers</strong></li>
</ul>
</div>
@@ -218,9 +217,9 @@ const PrivacyPolicy = () => {
</p>
</div>
<div className="border-l-4 border-green-500 pl-4">
<h3 className="font-semibold text-slate-800 dark:text-white">Google AdSense (Future)</h3>
<h3 className="font-semibold text-slate-800 dark:text-white">Adsterra</h3>
<p className="text-slate-600 dark:text-slate-300 text-sm">
Privacy Policy: <a href="https://policies.google.com/privacy" className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">https://policies.google.com/privacy</a>
Privacy Policy: <a href="https://adsterra.com/privacy-policy/" className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">https://adsterra.com/privacy-policy/</a>
</p>
</div>
</div>

View File

@@ -126,7 +126,7 @@ const TermsOfService = () => {
</h2>
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4">
<p className="text-amber-800 dark:text-amber-200 leading-relaxed">
<strong>Transparency Notice:</strong> We plan to implement Google AdSense advertisements in the future to support the free operation of this service. When implemented, ads will be clearly marked and will not interfere with tool functionality. Our privacy-first approach will remain unchanged - we will never sell or share your usage data with advertisers.
<strong>Transparency Notice:</strong> We display Adsterra advertisements to support the free operation of this service. Ads are clearly marked and do not interfere with tool functionality. Our privacy-first approach remains unchanged - we will never sell or share your usage data with advertisers.
</p>
</div>
</section>

View File

@@ -2,7 +2,6 @@ import React, { useState, useEffect, useRef } from 'react';
import { Copy } from 'lucide-react';
const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameRef, selectedElementInfo }) => {
console.log('🔍 ELEMENT EDITOR: Received props:', { selectedElementInfo, previewFrameRef: !!previewFrameRef });
const [edited, setEdited] = useState(null);
const textareaRefs = useRef({});
@@ -26,7 +25,6 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
});
setEdited(elementInfo);
console.log('🎯 ENHANCED EDITOR: Initialized with selected element:', elementInfo);
} else {
// Clear the editor when no element is selected
setEdited(null);
@@ -62,7 +60,6 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
// ENHANCED OPTION A: Field change handler using PreviewFrame API
const handleFieldChange = (field, value) => {
console.log(`⌨️ ENHANCED: Field '${field}' changed to '${value}'`);
setEdited(prev => ({ ...prev, [field]: value }));
// Use Enhanced Option A API for direct DOM manipulation
@@ -80,19 +77,13 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
success = previewFrameRef.current.updateElementStyle(edited.cascadeId, field, value);
}
if (success) {
console.log(`✅ ENHANCED UPDATE: ${field} updated in iframe DOM (scroll preserved)`);
} else {
console.error(`❌ ENHANCED UPDATE: Failed to update ${field}`);
if (!success) {
// Silently handle failure
}
} else {
console.warn('⚠️ ENHANCED UPDATE: PreviewFrame ref or cascadeId not available');
}
};
const handleSave = () => {
console.log('💾 ENHANCED OPTION A SAVE: Using PreviewFrame API to get iframe content');
try {
// Use Enhanced Option A API to get iframe content
if (previewFrameRef?.current?.getIframeContent) {
@@ -110,17 +101,11 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
// ENHANCED OPTION A: Update htmlInput with the cleaned iframe content
// This is the ONLY allowed setHtmlInput call during inspector operations (explicit save)
setHtmlInput(cleanedHtml);
console.log('✅ ENHANCED SAVE: Successfully extracted and cleaned iframe DOM via API');
} else {
console.warn('⚠️ Save: No content returned from PreviewFrame API');
}
} else {
console.error('❌ Save: PreviewFrame API not available');
}
} catch (error) {
console.error('❌ Enhanced Option A Save failed:', error);
// Silently handle error
}
// Trigger parent save callback
@@ -133,12 +118,9 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
// 5. ENHANCED OPTION A: Cancel reverts changes in iframe DOM directly
const handleCancel = () => {
console.log('🔄 ENHANCED CANCEL: Reverting changes in iframe DOM without triggering refresh');
// For Enhanced Option A, we don't need to revert HTML input
// The iframe DOM changes will be discarded when the inspector closes
// Just close the inspector without syncing changes
console.log('✅ ENHANCED CANCEL: Changes discarded, iframe DOM remains stable');
onClose();
};
@@ -158,9 +140,8 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
const elementString = `<${edited.tagName}${edited.id ? ` id="${edited.id}"` : ''}${edited.className ? ` class="${edited.className}"` : ''}${otherAttributes.map(attr => ` ${attr}="${edited[attr]}"`).join('')}>${edited.innerHTML || ''}</${edited.tagName}>`;
await navigator.clipboard.writeText(elementString);
console.log('✅ Element copied to clipboard');
} catch (err) {
console.error('❌ Failed to copy element:', err);
// Silently handle error
}
};

View File

@@ -1,674 +0,0 @@
import React, { useRef, useEffect, useCallback, useMemo, useState } from 'react';
// Device Frame CSS - Converted from SCSS
const deviceFrameCSS = `
/* iPhone 14 Pro Device Frame */
.device-iphone-14-pro {
height: 780px;
width: 384px;
transform-origin: center;
position: relative;
margin: 0 auto;
}
.device-iphone-14-pro .device-frame {
background: #010101;
border: 1px solid #2a242f;
border-radius: 61px;
box-shadow: inset 0 0 4px 2px #a8a4b0, inset 0 0 0 5px #342C3F;
height: 780px;
padding: 17px;
width: 384px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.device-iphone-14-pro .device-screen {
border-radius: 56px;
height: 746px;
width: 350px;
overflow: hidden;
scale: 0.75;
min-width: 130%;
height: 130%;
}
.device-iphone-14-pro .device-screen iframe {
width: 130%; /* 100% / 0.75 = 133.33% to compensate for 0.75 scale */
height: 130%;
transform: scale(0.75);
transform-origin: top left;
}
/* Mobile scrollbar styling for iPhone */
.device-iphone-14-pro .device-screen::-webkit-scrollbar {
width: 2px;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 1px;
}
.device-iphone-14-pro .device-stripe::after,
.device-iphone-14-pro .device-stripe::before {
border: solid rgba(1, 1, 1, 0.25);
border-width: 0 7px;
content: "";
height: 7px;
left: 0;
position: absolute;
width: 100%;
z-index: 9;
}
.device-iphone-14-pro .device-stripe::after {
top: 77px;
}
.device-iphone-14-pro .device-stripe::before {
bottom: 77px;
}
.device-iphone-14-pro .device-header {
background: #010101;
border-radius: 18px;
height: 31px;
left: 50%;
margin-left: -54px;
position: absolute;
top: 32px;
width: 108px;
z-index: 10;
}
.device-iphone-14-pro .device-sensors::after,
.device-iphone-14-pro .device-sensors::before {
content: "";
position: absolute;
}
.device-iphone-14-pro .device-sensors::after {
background: #010101;
border-radius: 16px;
height: 30px;
left: 50%;
margin-left: -54px;
top: 33px;
width: 67px;
z-index: 10;
}
.device-iphone-14-pro .device-sensors::before {
background: radial-gradient(farthest-corner at 20% 20%, #6074BF 0, transparent 40%),
radial-gradient(farthest-corner at 80% 80%, #513785 0, #24555E 20%, transparent 50%);
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.05);
border-radius: 50%;
height: 8px;
left: 50%;
margin-left: 24px;
top: 44px;
width: 8px;
z-index: 10;
}
.device-iphone-14-pro .device-btns {
background: #2a242f;
border-radius: 1px;
height: 24px;
left: -2px;
position: absolute;
top: 86px;
width: 2px;
}
.device-iphone-14-pro .device-btns::after,
.device-iphone-14-pro .device-btns::before {
background: #2a242f;
border-radius: 1px;
content: "";
height: 46px;
left: 0;
position: absolute;
width: 2px;
}
.device-iphone-14-pro .device-btns::after {
top: 45px;
}
.device-iphone-14-pro .device-btns::before {
top: 105px;
}
.device-iphone-14-pro .device-power {
background: #2a242f;
border-radius: 1px;
height: 75px;
right: -2px;
position: absolute;
top: 150px;
width: 2px;
}
/* iPad Pro Device Frame */
.device-ipad-pro {
height: 840px;
width: 600px;
transform-origin: center;
margin-top: 40px;
position: relative;
margin-left: auto;
margin-right: auto;
}
.device-ipad-pro .device-frame {
background: #0d0d0d;
border-radius: 32px;
box-shadow: inset 0 0 0 1px #c1c2c3, inset 0 0 1px 2px #e2e3e4;
height: 800px;
padding: 24px;
width: 576px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.device-ipad-pro .device-screen {
border: 2px solid #0f0f0f;
border-radius: 10px;
overflow: hidden;
min-width: 200%;
height: 200%;
scale: 0.5;
}
.device-ipad-pro .device-screen iframe {
/* Set the iframe to the actual device resolution and scale it down */
width: 834px; /* iPad Pro 11" logical width */
height: 1194px; /* iPad Pro 11" logical height */
transform: scale(0.6331); /* 528px (screen width) / 834px (logical width) */
transform-origin: top left;
background: #fff; /* Ensure bg color for content */
}
/* Mobile scrollbar styling for iPad */
.device-ipad-pro .device-screen::-webkit-scrollbar {
width: 3px;
}
.device-ipad-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-ipad-pro .device-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
.device-ipad-pro .device-power {
background: #2a242f;
border-radius: 2px;
height: 2px;
width: 38px;
right: 76px;
top: -2px;
position: absolute;
}
/* Reposition buttons specifically for iPad Pro */
.device-ipad-pro .device-btns {
background: #2a242f;
border-radius: 2px;
height: 30px; /* Volume up */
width: 2px;
right: 22px;
top: 90px;
position: absolute;
}
.device-ipad-pro .device-btns::after {
content: "";
background: #2a242f;
border-radius: 2px;
height: 30px; /* Volume down */
width: 2px;
left: 0;
top: 40px; /* Space between buttons */
position: absolute;
}
.device-ipad-pro .device-btns::before {
display: none; /* Hide the third button from iPhone */
}
.device-ipad-pro .device-sensors::after,
.device-ipad-pro .device-sensors::before {
content: "";
position: absolute;
}
.device-ipad-pro .device-sensors::after {
background: #141414;
border-radius: 16px;
box-shadow: -18px 0 #141414, 64px 0 #141414;
height: 10px;
left: 50%;
margin-left: -28px;
top: 11px;
width: 10px;
}
.device-ipad-pro .device-sensors::before {
background: radial-gradient(farthest-corner at 20% 20%, #6074BF 0, transparent 40%),
radial-gradient(farthest-corner at 80% 80%, #513785 0, #24555E 20%, transparent 50%);
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.05);
border-radius: 50%;
height: 6px;
left: 50%;
margin-left: -3px;
top: 13px;
width: 5px;
}
/* Enable smooth scrolling on iOS */
.device-iphone-14-pro .device-screen,
.device-ipad-pro .device-screen {
-webkit-overflow-scrolling: touch; /* smooth momentum scroll on iOS */
overflow-y: auto;
}
/* Mobile custom scrollbar */
.device-iphone-14-pro .device-screen::-webkit-scrollbar,
.device-ipad-pro .device-screen::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-track,
.device-ipad-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-thumb,
.device-ipad-pro .device-screen::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* Optional: Hide scrollbar on larger screens for desktop */
/* This media query hides the scrollbar on desktops where touch scrolling is not needed */
@media (pointer: fine) and (hover: hover) {
.device-iphone-14-pro .device-screen::-webkit-scrollbar,
.device-ipad-pro .device-screen::-webkit-scrollbar {
display: none;
}
}
`;
const PreviewFrame = ({
htmlInput,
cssInput,
jsInput,
selectedDevice,
inspectMode,
onElementClick,
isFullscreen
}) => {
const iframeRef = useRef(null);
// Handle iframe click for element selection - defined first to avoid initialization errors
const handleIframeClick = useCallback((e) => {
if (!inspectMode) return;
e.preventDefault();
e.stopPropagation();
const clickedElement = e.target;
const elementInfo = {
tagName: clickedElement.tagName.toLowerCase(),
innerText: clickedElement.innerText || clickedElement.textContent || '',
attributes: {}
};
Array.from(clickedElement.attributes).forEach(attr => {
elementInfo.attributes[attr.name] = attr.value;
});
onElementClick(elementInfo);
}, [inspectMode, onElementClick]);
// Function to setup inspect mode styles and event handlers with MutationObserver
const setupInspectModeStyles = useCallback((iframeDoc) => {
console.log('🎨 PreviewFrame: Setting up robust inspect mode with MutationObserver');
// Remove existing inspect styles and observers
const existingStyle = iframeDoc.getElementById('inspect-mode-styles');
if (existingStyle) existingStyle.remove();
// Clean up any existing observer
if (iframeDoc._inspectObserver) {
iframeDoc._inspectObserver.disconnect();
delete iframeDoc._inspectObserver;
}
// Add inspect mode styles with better hover highlights
const style = iframeDoc.createElement('style');
style.id = 'inspect-mode-styles';
style.textContent = `
/* High specificity selectors for inspect mode */
html * {
cursor: crosshair !important;
pointer-events: auto !important;
}
/* Hover highlights with maximum specificity */
html body *:hover {
outline: 2px solid #3b82f6 !important;
outline-offset: 1px !important;
background-color: rgba(59, 130, 246, 0.1) !important;
transition: all 0.1s ease !important;
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.3) !important;
}
/* Selected element styles */
html body [data-original="false"] {
outline: 2px solid #10b981 !important;
outline-offset: 1px !important;
background-color: rgba(16, 185, 129, 0.1) !important;
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.3) !important;
}
/* Selected element hover */
html body [data-original="false"]:hover {
outline: 2px solid #059669 !important;
outline-offset: 1px !important;
background-color: rgba(5, 150, 105, 0.15) !important;
box-shadow: 0 0 0 1px rgba(5, 150, 105, 0.4) !important;
}
/* Prevent text selection during inspect */
html * {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
/* Override any existing hover styles */
html body *:hover {
border: none !important;
}
`;
iframeDoc.head.appendChild(style);
// Add event handlers
const preventInteraction = (e) => {
e.preventDefault();
e.stopPropagation();
return false;
};
// Add click handler for element selection
iframeDoc.addEventListener('click', handleIframeClick, true);
// Prevent other interactions during inspect mode
iframeDoc.addEventListener('mousedown', preventInteraction, true);
iframeDoc.addEventListener('mouseup', preventInteraction, true);
iframeDoc.addEventListener('contextmenu', preventInteraction, true);
iframeDoc.addEventListener('dragstart', preventInteraction, true);
iframeDoc.addEventListener('selectstart', preventInteraction, true);
// Setup MutationObserver to reapply styles when DOM changes
const observer = new MutationObserver((mutations) => {
let needsReapply = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
needsReapply = true;
}
});
if (needsReapply) {
console.log('🔄 PreviewFrame: DOM changed, reapplying inspect styles');
// Reapply styles after a short delay to ensure new elements are rendered
setTimeout(() => {
const currentStyle = iframeDoc.getElementById('inspect-mode-styles');
if (!currentStyle) {
// Style was removed, reapply it
const newStyle = iframeDoc.createElement('style');
newStyle.id = 'inspect-mode-styles';
newStyle.textContent = style.textContent;
iframeDoc.head.appendChild(newStyle);
}
}, 50);
}
});
// Start observing DOM changes
observer.observe(iframeDoc.body, {
childList: true,
subtree: true,
attributes: false
});
// Store observer for cleanup
iframeDoc._inspectObserver = observer;
console.log('✅ PreviewFrame: Robust inspect mode with MutationObserver applied');
}, [handleIframeClick]);
const generateHtmlContent = useCallback(() => {
const scrollbarCSS = `
@media (max-width: 960px) {
html, body {
overflow-y: overlay; /* scrollbar overlays content, no space reserved */
-webkit-overflow-scrolling: touch;
padding-right: 0; /* No padding, since scrollbar overlays */
box-sizing: border-box; /* Use border-box for correct sizing */
scrollbar-width: thin;
scrollbar-color: rgba(0,0,0,0.2) transparent;
}
body::-webkit-scrollbar {
width: 0;
background: transparent;
}
/* Custom scrollbar styles */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 3px;
border: 1px solid rgba(0,0,0,0.1);
}
/* Firefox custom scrollbar */
html {
scrollbar-width: thin;
scrollbar-color: rgba(0,0,0,0.2) transparent;
}
}
`;
const finalCss = `${scrollbarCSS}\n${cssInput || ''}`;
const isFullDocument = htmlInput.trim().toLowerCase().includes('<html');
if (isFullDocument) {
let content = htmlInput;
const headEndIndex = content.toLowerCase().indexOf('</head>');
if (headEndIndex !== -1) {
const styleTag = `\n<style>\n${finalCss}\n</style>\n`;
content = content.slice(0, headEndIndex) + styleTag + content.slice(headEndIndex);
}
if (jsInput && jsInput.trim()) {
const bodyEndIndex = content.toLowerCase().lastIndexOf('</body>');
if (bodyEndIndex !== -1) {
const scriptTag = `\n<script>\n(function() {\n${jsInput}\n})();\n</script>\n`;
content = content.slice(0, bodyEndIndex) + scriptTag + content.slice(bodyEndIndex);
}
}
return content;
} else {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preview</title>
<style>${finalCss}</style>
</head>
<body>
${htmlInput}
<script>
(function() {
${jsInput || ''}
})();
</script>
</body>
</html>
`;
}
}, [htmlInput, cssInput, jsInput]);
// Effect for loading content and managing inspect mode
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const content = generateHtmlContent();
const doc = iframe.contentDocument || iframe.contentWindow.document;
// Write content
doc.open();
doc.write(content);
doc.close();
// Apply inspect mode immediately after writing content
if (inspectMode) {
console.log('🎨 Applying inspect mode styles.');
setupInspectModeStyles(doc);
}
}, [htmlInput, cssInput, jsInput, inspectMode, selectedDevice, isFullscreen, generateHtmlContent, setupInspectModeStyles]);
// Effect for injecting device frame CSS into the main document
useEffect(() => {
const styleId = 'device-frame-styles';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = deviceFrameCSS;
document.head.appendChild(style);
}
return () => {
const style = document.getElementById(styleId);
if (style) {
style.remove();
}
};
}, []);
const getDeviceWrapper = () => {
console.log('🔧 Device Frame Debug:', { isFullscreen, selectedDevice });
// Non-fullscreen always uses iPhone frame (mobile view)
if (!isFullscreen) {
console.log('📱 Non-fullscreen: Using iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
}
// Fullscreen desktop mode: no frame
if (selectedDevice === 'desktop') {
console.log('🖥️ Desktop fullscreen: No device frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
};
}
switch (selectedDevice) {
case 'tablet':
console.log('📟 Rendering iPad Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'ipad-pro'
};
case 'mobile':
console.log('📱 Rendering iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
default:
console.log('❓ Unknown device, no frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
};
}
};
const { wrapperClass, deviceFrame } = getDeviceWrapper();
if (deviceFrame) {
// Render with device frame (iPhone 14 Pro or iPad Pro)
console.log(`🎨 Rendering device frame: device-${deviceFrame}`);
return (
<div className={wrapperClass}>
<div className={`device device-${deviceFrame}`}>
<div className="device-frame">
<iframe
ref={iframeRef}
key={`device-${deviceFrame}-${selectedDevice}-${isFullscreen}`}
className="device-screen w-full h-full border-0"
title="HTML Preview"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
/>
</div>
<div className="device-stripe"></div>
<div className="device-header"></div>
<div className="device-sensors"></div>
<div className="device-btns"></div>
<div className="device-power"></div>
</div>
</div>
);
}
// Render without device frame (desktop or non-fullscreen)
return (
<div className={`${wrapperClass} bg-white rounded-lg shadow-lg`}>
<iframe
ref={iframeRef}
key={`no-device-${selectedDevice}-${isFullscreen}`}
className="w-full h-full border-0 overflow-hidden"
title="HTML Preview"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
style={{ maxWidth: '100%', maxHeight: '100%' }}
/>
</div>
);
};
export default PreviewFrame;

View File

@@ -1,720 +0,0 @@
import React, { useRef, useEffect, useCallback, useMemo, useState } from 'react';
// Device Frame CSS - Converted from SCSS
const deviceFrameCSS = `
/* iPhone 14 Pro Device Frame */
.device-iphone-14-pro {
height: 780px;
width: 384px;
transform-origin: center;
position: relative;
margin: 0 auto;
}
.device-iphone-14-pro .device-frame {
background: #010101;
border: 1px solid #2a242f;
border-radius: 61px;
box-shadow: inset 0 0 4px 2px #a8a4b0, inset 0 0 0 5px #342C3F;
height: 780px;
padding: 17px;
width: 384px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.device-iphone-14-pro .device-screen {
border-radius: 56px;
height: 746px;
width: 350px;
overflow: hidden;
scale: 0.75;
min-width: 130%;
height: 130%;
}
.device-iphone-14-pro .device-screen iframe {
width: 130%; /* 100% / 0.75 = 133.33% to compensate for 0.75 scale */
height: 130%;
transform: scale(0.75);
transform-origin: top left;
}
/* Mobile scrollbar styling for iPhone */
.device-iphone-14-pro .device-screen::-webkit-scrollbar {
width: 2px;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 1px;
}
.device-iphone-14-pro .device-stripe::after,
.device-iphone-14-pro .device-stripe::before {
border: solid rgba(1, 1, 1, 0.25);
border-width: 0 7px;
content: "";
height: 7px;
left: 0;
position: absolute;
width: 100%;
z-index: 9;
}
.device-iphone-14-pro .device-stripe::after {
top: 77px;
}
.device-iphone-14-pro .device-stripe::before {
bottom: 77px;
}
.device-iphone-14-pro .device-header {
background: #010101;
border-radius: 18px;
height: 31px;
left: 50%;
margin-left: -54px;
position: absolute;
top: 32px;
width: 108px;
z-index: 10;
}
.device-iphone-14-pro .device-sensors::after,
.device-iphone-14-pro .device-sensors::before {
content: "";
position: absolute;
}
.device-iphone-14-pro .device-sensors::after {
background: #010101;
border-radius: 16px;
height: 30px;
left: 50%;
margin-left: -54px;
top: 33px;
width: 67px;
z-index: 10;
}
.device-iphone-14-pro .device-sensors::before {
background: radial-gradient(farthest-corner at 20% 20%, #6074BF 0, transparent 40%),
radial-gradient(farthest-corner at 80% 80%, #513785 0, #24555E 20%, transparent 50%);
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.05);
border-radius: 50%;
height: 8px;
left: 50%;
margin-left: 24px;
top: 44px;
width: 8px;
z-index: 10;
}
.device-iphone-14-pro .device-btns {
background: #2a242f;
border-radius: 1px;
height: 24px;
left: -2px;
position: absolute;
top: 86px;
width: 2px;
}
.device-iphone-14-pro .device-btns::after,
.device-iphone-14-pro .device-btns::before {
background: #2a242f;
border-radius: 1px;
content: "";
height: 46px;
left: 0;
position: absolute;
width: 2px;
}
.device-iphone-14-pro .device-btns::after {
top: 45px;
}
.device-iphone-14-pro .device-btns::before {
top: 105px;
}
.device-iphone-14-pro .device-power {
background: #2a242f;
border-radius: 1px;
height: 75px;
right: -2px;
position: absolute;
top: 150px;
width: 2px;
}
/* iPad Pro Device Frame */
.device-ipad-pro {
height: 840px;
width: 600px;
transform-origin: center;
margin-top: 40px;
position: relative;
margin-left: auto;
margin-right: auto;
}
.device-ipad-pro .device-frame {
background: #0d0d0d;
border-radius: 32px;
box-shadow: inset 0 0 0 1px #c1c2c3, inset 0 0 1px 2px #e2e3e4;
height: 800px;
padding: 24px;
width: 576px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.device-ipad-pro .device-screen {
border: 2px solid #0f0f0f;
border-radius: 10px;
overflow: hidden;
min-width: 200%;
height: 200%;
scale: 0.5;
}
.device-ipad-pro .device-screen iframe {
/* Set the iframe to the actual device resolution and scale it down */
width: 834px; /* iPad Pro 11" logical width */
height: 1194px; /* iPad Pro 11" logical height */
transform: scale(0.6331); /* 528px (screen width) / 834px (logical width) */
transform-origin: top left;
background: #fff; /* Ensure bg color for content */
}
/* Mobile scrollbar styling for iPad */
.device-ipad-pro .device-screen::-webkit-scrollbar {
width: 3px;
}
.device-ipad-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-ipad-pro .device-screen::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 2px;
}
.device-ipad-pro .device-power {
background: #2a242f;
border-radius: 2px;
height: 2px;
width: 38px;
right: 76px;
top: -2px;
position: absolute;
}
/* Reposition buttons specifically for iPad Pro */
.device-ipad-pro .device-btns {
background: #2a242f;
border-radius: 2px;
height: 30px; /* Volume up */
width: 2px;
right: 22px;
top: 90px;
position: absolute;
}
.device-ipad-pro .device-btns::after {
content: "";
background: #2a242f;
border-radius: 2px;
height: 30px; /* Volume down */
width: 2px;
left: 0;
top: 40px; /* Space between buttons */
position: absolute;
}
.device-ipad-pro .device-btns::before {
display: none; /* Hide the third button from iPhone */
}
.device-ipad-pro .device-sensors::after,
.device-ipad-pro .device-sensors::before {
content: "";
position: absolute;
}
.device-ipad-pro .device-sensors::after {
background: #141414;
border-radius: 16px;
box-shadow: -18px 0 #141414, 64px 0 #141414;
height: 10px;
left: 50%;
margin-left: -28px;
top: 11px;
width: 10px;
}
.device-ipad-pro .device-sensors::before {
background: radial-gradient(farthest-corner at 20% 20%, #6074BF 0, transparent 40%),
radial-gradient(farthest-corner at 80% 80%, #513785 0, #24555E 20%, transparent 50%);
box-shadow: 0 0 1px 1px rgba(255, 255, 255, 0.05);
border-radius: 50%;
height: 6px;
left: 50%;
margin-left: -3px;
top: 13px;
width: 5px;
}
/* Enable smooth scrolling on iOS */
.device-iphone-14-pro .device-screen,
.device-ipad-pro .device-screen {
-webkit-overflow-scrolling: touch; /* smooth momentum scroll on iOS */
overflow-y: auto;
}
/* Mobile custom scrollbar */
.device-iphone-14-pro .device-screen::-webkit-scrollbar,
.device-ipad-pro .device-screen::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-track,
.device-ipad-pro .device-screen::-webkit-scrollbar-track {
background: transparent;
}
.device-iphone-14-pro .device-screen::-webkit-scrollbar-thumb,
.device-ipad-pro .device-screen::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* Optional: Hide scrollbar on larger screens for desktop */
/* This media query hides the scrollbar on desktops where touch scrolling is not needed */
@media (pointer: fine) and (hover: hover) {
.device-iphone-14-pro .device-screen::-webkit-scrollbar,
.device-ipad-pro .device-screen::-webkit-scrollbar {
display: none;
}
}
`;
const PreviewFrameExperimental = ({
htmlInput,
cssInput,
jsInput,
selectedDevice,
inspectMode,
onElementClick,
isFullscreen
}) => {
const iframeRef = useRef(null);
const [isInitialized, setIsInitialized] = useState(false);
const [lastJsInput, setLastJsInput] = useState('');
// Handle iframe click for element selection
const handleIframeClick = useCallback((e) => {
if (!inspectMode) return;
e.preventDefault();
e.stopPropagation();
const clickedElement = e.target;
const elementInfo = {
tagName: clickedElement.tagName.toLowerCase(),
innerText: clickedElement.innerText || clickedElement.textContent || '',
attributes: {}
};
Array.from(clickedElement.attributes).forEach(attr => {
elementInfo.attributes[attr.name] = attr.value;
});
onElementClick(elementInfo);
}, [inspectMode, onElementClick]);
// Function to setup inspect mode styles and event handlers
const setupInspectModeStyles = useCallback((iframeDoc) => {
console.log('🎨 PreviewFrame Experimental: Setting up inspect mode');
// Remove existing inspect styles and observers
const existingStyle = iframeDoc.getElementById('inspect-mode-styles');
if (existingStyle) existingStyle.remove();
// Clean up any existing observer
if (iframeDoc._inspectObserver) {
iframeDoc._inspectObserver.disconnect();
delete iframeDoc._inspectObserver;
}
// Add inspect mode styles with better hover highlights
const style = iframeDoc.createElement('style');
style.id = 'inspect-mode-styles';
style.textContent = `
/* High specificity selectors for inspect mode */
html * {
cursor: crosshair !important;
pointer-events: auto !important;
}
/* Hover highlights with maximum specificity */
html body *:hover {
outline: 2px solid #3b82f6 !important;
outline-offset: 1px !important;
background-color: rgba(59, 130, 246, 0.1) !important;
transition: all 0.1s ease !important;
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.3) !important;
}
/* Selected element styles */
html body [data-selected="true"] {
outline: 2px solid #10b981 !important;
outline-offset: 1px !important;
background-color: rgba(16, 185, 129, 0.1) !important;
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.3) !important;
}
/* Selected element hover */
html body [data-selected="true"]:hover {
outline: 2px solid #059669 !important;
outline-offset: 1px !important;
background-color: rgba(5, 150, 105, 0.15) !important;
box-shadow: 0 0 0 1px rgba(5, 150, 105, 0.4) !important;
}
/* Prevent text selection during inspect */
html * {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
`;
iframeDoc.head.appendChild(style);
// Add event handlers
const preventInteraction = (e) => {
e.preventDefault();
e.stopPropagation();
return false;
};
// Enhanced click handler that marks selected elements
const enhancedClickHandler = (e) => {
if (!inspectMode) return;
e.preventDefault();
e.stopPropagation();
// Remove previous selection
const previousSelected = iframeDoc.querySelector('[data-selected="true"]');
if (previousSelected) {
previousSelected.removeAttribute('data-selected');
}
// Mark new selection
const clickedElement = e.target;
clickedElement.setAttribute('data-selected', 'true');
// Call the original handler
handleIframeClick(e);
};
// Add click handler for element selection
iframeDoc.addEventListener('click', enhancedClickHandler, true);
// Prevent other interactions during inspect mode
iframeDoc.addEventListener('mousedown', preventInteraction, true);
iframeDoc.addEventListener('mouseup', preventInteraction, true);
iframeDoc.addEventListener('contextmenu', preventInteraction, true);
iframeDoc.addEventListener('dragstart', preventInteraction, true);
iframeDoc.addEventListener('selectstart', preventInteraction, true);
console.log('✅ PreviewFrame Experimental: Inspect mode applied');
}, [handleIframeClick, inspectMode]);
// Generate base HTML structure (without user JS)
const generateBaseHtml = useCallback(() => {
const scrollbarCSS = `
/* Custom Scrollbar Styles */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #555; }
html { scrollbar-width: thin; scrollbar-color: #888 #f1f1f1; }
`;
const finalCss = `${scrollbarCSS}\n${cssInput || ''}`;
const isFullDocument = htmlInput.trim().toLowerCase().includes('<html');
if (isFullDocument) {
let content = htmlInput;
const headEndIndex = content.toLowerCase().indexOf('</head>');
if (headEndIndex !== -1) {
const styleTag = `\n<style>\n${finalCss}\n</style>\n`;
content = content.slice(0, headEndIndex) + styleTag + content.slice(headEndIndex);
}
return content;
} else {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preview</title>
<style>${finalCss}</style>
</head>
<body>
${htmlInput}
</body>
</html>
`;
}
}, [htmlInput, cssInput]);
// Initialize iframe with base content (runs once or when device changes)
const initializeIframe = useCallback(() => {
const iframe = iframeRef.current;
if (!iframe) return;
console.log('🚀 PreviewFrame Experimental: Initializing iframe');
const content = generateBaseHtml();
const doc = iframe.contentDocument || iframe.contentWindow.document;
// Write base content
doc.open();
doc.write(content);
doc.close();
setIsInitialized(true);
setLastJsInput(''); // Reset JS tracking
console.log('✅ PreviewFrame Experimental: Base iframe initialized');
}, [generateBaseHtml]);
// Update only JavaScript content (without full page reload)
const updateJavaScript = useCallback(() => {
if (!isInitialized || !jsInput || jsInput === lastJsInput) return;
const iframe = iframeRef.current;
if (!iframe) return;
console.log('📝 PreviewFrame Experimental: Updating JavaScript only');
const doc = iframe.contentDocument || iframe.contentWindow.document;
// Remove previous script if exists
const existingScript = doc.getElementById('user-script');
if (existingScript) {
existingScript.remove();
}
// Add new script with IIFE wrapper
if (jsInput.trim()) {
const script = doc.createElement('script');
script.id = 'user-script';
script.textContent = `
(function() {
try {
${jsInput}
} catch(e) {
console.error('User script error:', e);
}
})();
`;
doc.body.appendChild(script);
}
setLastJsInput(jsInput);
console.log('✅ PreviewFrame Experimental: JavaScript updated');
}, [isInitialized, jsInput, lastJsInput]);
// Update HTML content (requires partial reload)
const updateHtmlContent = useCallback(() => {
if (!isInitialized) return;
const iframe = iframeRef.current;
if (!iframe) return;
console.log('🔄 PreviewFrame Experimental: Updating HTML content');
const doc = iframe.contentDocument || iframe.contentWindow.document;
// Update body content only (preserve head and scripts)
const isFullDocument = htmlInput.trim().toLowerCase().includes('<html');
if (!isFullDocument) {
doc.body.innerHTML = htmlInput;
// Re-add user script after HTML update
updateJavaScript();
// Re-apply inspect mode if enabled
if (inspectMode) {
setupInspectModeStyles(doc);
}
} else {
// For full documents, we need to reinitialize
initializeIframe();
}
console.log('✅ PreviewFrame Experimental: HTML content updated');
}, [isInitialized, htmlInput, inspectMode, updateJavaScript, setupInspectModeStyles, initializeIframe]);
// Effect for iframe initialization (runs when device changes)
useEffect(() => {
initializeIframe();
}, [selectedDevice, isFullscreen, initializeIframe]);
// Effect for HTML content updates
useEffect(() => {
if (isInitialized) {
updateHtmlContent();
}
}, [htmlInput, cssInput, updateHtmlContent, isInitialized]);
// Effect for JavaScript updates
useEffect(() => {
updateJavaScript();
}, [jsInput, updateJavaScript]);
// Effect for inspect mode
useEffect(() => {
if (!isInitialized) return;
const iframe = iframeRef.current;
if (!iframe) return;
const doc = iframe.contentDocument || iframe.contentWindow.document;
if (inspectMode) {
console.log('🎨 PreviewFrame Experimental: Enabling inspect mode');
setupInspectModeStyles(doc);
} else {
console.log('🎨 PreviewFrame Experimental: Disabling inspect mode');
const existingStyle = doc.getElementById('inspect-mode-styles');
if (existingStyle) existingStyle.remove();
// Remove selected attributes
const selectedElements = doc.querySelectorAll('[data-selected="true"]');
selectedElements.forEach(el => el.removeAttribute('data-selected'));
}
}, [inspectMode, isInitialized, setupInspectModeStyles]);
// Effect for injecting device frame CSS into the main document
useEffect(() => {
const styleId = 'device-frame-styles';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = deviceFrameCSS;
document.head.appendChild(style);
}
return () => {
const style = document.getElementById(styleId);
if (style) {
style.remove();
}
};
}, []);
const getDeviceWrapper = () => {
console.log('🔧 Device Frame Debug (Experimental):', { isFullscreen, selectedDevice });
// Non-fullscreen always uses iPhone frame (mobile view)
if (!isFullscreen) {
console.log('📱 Non-fullscreen: Using iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
}
// Fullscreen desktop mode: no frame
if (selectedDevice === 'desktop') {
console.log('🖥️ Desktop fullscreen: No device frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
};
}
switch (selectedDevice) {
case 'tablet':
console.log('📟 Rendering iPad Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'ipad-pro'
};
case 'mobile':
console.log('📱 Rendering iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
default:
console.log('❓ Unknown device, no frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
};
}
};
const { wrapperClass, deviceFrame } = getDeviceWrapper();
if (deviceFrame) {
// Render with device frame (iPhone 14 Pro or iPad Pro)
console.log(`🎨 Rendering device frame (Experimental): device-${deviceFrame}`);
return (
<div className={wrapperClass}>
<div className={`device device-${deviceFrame}`}>
<div className="device-frame">
<iframe
ref={iframeRef}
key={`experimental-device-${deviceFrame}-${selectedDevice}-${isFullscreen}`}
className="device-screen w-full h-full border-0"
title="HTML Preview (Experimental)"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
/>
</div>
<div className="device-stripe"></div>
<div className="device-header"></div>
<div className="device-sensors"></div>
<div className="device-btns"></div>
<div className="device-power"></div>
</div>
</div>
);
}
// Render without device frame (desktop or non-fullscreen)
return (
<div className={`${wrapperClass} bg-white rounded-lg shadow-lg`}>
<iframe
ref={iframeRef}
key={`experimental-no-device-${selectedDevice}-${isFullscreen}`}
className="w-full h-full border-0 overflow-hidden"
title="HTML Preview (Experimental)"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
style={{ maxWidth: '100%', maxHeight: '100%' }}
/>
</div>
);
};
export default PreviewFrameExperimental;

View File

@@ -345,13 +345,11 @@ const PreviewFrame = ({
// Mark new selection
const clickedElement = e.target;
console.log(`🎯 ELEMENT SELECT: Clicked on <${clickedElement.tagName.toLowerCase()}> element`);
clickedElement.setAttribute('data-selected', 'true');
// Assign unique cascade-id for inspector operations
const cascadeId = `cascade-${Date.now()}`;
clickedElement.setAttribute('data-cascade-id', cascadeId);
console.log(`🏷️ ELEMENT SELECT: Assigned cascade-id: ${cascadeId}`);
const elementInfo = {
tagName: clickedElement.tagName.toLowerCase(),
@@ -372,9 +370,8 @@ const PreviewFrame = ({
} else {
window.currentIframeDom = updatedHtml;
}
console.log('💾 ELEMENT SELECT: Stored current iframe DOM with cascade-id');
} catch (error) {
console.warn('⚠️ Could not store iframe DOM:', error);
// silently handle iframe DOM storage failures
}
onElementClick(elementInfo);
@@ -382,7 +379,6 @@ const PreviewFrame = ({
// Function to setup inspect mode styles and event handlers with MutationObserver
const setupInspectModeStyles = useCallback((iframeDoc) => {
console.log('🎨 PreviewFrame: Setting up robust inspect mode with MutationObserver');
// Remove existing inspect styles and observers
const existingStyle = iframeDoc.getElementById('inspect-mode-styles');
@@ -485,7 +481,6 @@ const PreviewFrame = ({
});
if (needsReapply) {
console.log('🔄 PreviewFrame: DOM changed, reapplying inspect styles');
// Reapply styles after a short delay to ensure new elements are rendered
setTimeout(() => {
const currentStyle = iframeDoc.getElementById('inspect-mode-styles');
@@ -509,8 +504,6 @@ const PreviewFrame = ({
// Store observer for cleanup
iframeDoc._inspectObserver = observer;
console.log('✅ PreviewFrame: Robust inspect mode with MutationObserver applied');
}, [handleIframeClick]);
const generateHtmlContent = useCallback(() => {
@@ -674,18 +667,14 @@ const PreviewFrame = ({
const justCommitted = window.justCommittedInspectorChanges || false;
if (isInspectorActive && !isGenuineCodeChange) {
console.log('🚫 SKIP REFRESH: Inspector is active, iframe DOM is source of truth');
return;
}
if (justCommitted) {
console.log('🚫 SKIP REFRESH: Just committed inspector changes, preventing immediate refresh');
window.justCommittedInspectorChanges = false;
return;
}
console.log('✅ IFRAME UPDATE: Proceeding with iframe update (genuine code change or inspector inactive)');
// Update clean content refs
lastCleanHtmlRef.current = cleanHtml;
lastCleanCssRef.current = cleanCss;
@@ -717,7 +706,6 @@ const PreviewFrame = ({
// Skip update only if normalized content hasn't changed AND no device change
if (normalizedNew === normalizedLast && lastContentRef.current !== '' && !isDeviceChange) {
console.log('📋 Content unchanged and no device change, skipping update');
return;
}
@@ -740,7 +728,6 @@ const PreviewFrame = ({
// Add safety check for document readiness
if (!doc || !doc.body) {
console.log('⚠️ Document not ready for content update, using fallback...');
// Fallback to document.write for safety
if (iframe.contentDocument) {
iframe.contentDocument.open();
@@ -773,13 +760,10 @@ const PreviewFrame = ({
try {
// SMART SCROLL RESTORATION: Only restore for genuine code changes, not inspector operations
if (skipScrollRestorationRef.current) {
console.log('🚫 SKIP: Scroll restoration skipped for inspector operation');
skipScrollRestorationRef.current = false;
return;
}
console.log('🔄 SMART RESTORE: Attempting scroll restoration for genuine code change');
const storedScroll = window.localStorage.getItem('htmlPreview_scrollPosition');
if (!storedScroll) {
@@ -842,7 +826,6 @@ const PreviewFrame = ({
if (hasInspectorEdits && !isDeviceChange) {
// INSPECTOR FIELD EDIT - Visual-only iframe update (no scroll disruption)
console.log('📝 INSPECTOR SAVE: Updating iframe content');
// Skip scroll restoration for inspector edits
skipScrollRestorationRef.current = true;
@@ -865,20 +848,16 @@ const PreviewFrame = ({
editedElement.setAttribute(attr.name, attr.value);
}
});
console.log('🚀 Visual-only update complete - no scroll disruption');
}
}
} else {
// CODE/DEVICE CHANGE - Full content reload with scroll preservation
console.log('🔄 Code or device change: Full content reload');
const currentBody = doc.body;
const tempDiv = doc.createElement('div');
tempDiv.innerHTML = newContent.match(/<body[^>]*>([\s\S]*)<\/body>/i)?.[1] || newContent;
if (isDeviceChange) {
console.log('📱 Device change detected - canceling inspector session and fresh reload');
// Cancel any active inspector session by generating clean content from code boxes
const cleanContent = generateHtmlContent();
@@ -892,15 +871,12 @@ const PreviewFrame = ({
const restoreScrollAfterDeviceChange = () => {
try {
const storedScroll = localStorage.getItem('htmlPreview_scrollPosition');
console.log('📍 Device change: Attempting scroll restoration...', { storedScroll });
if (!storedScroll) {
console.log('⚠️ No stored scroll position found');
return;
}
if (!iframe.contentWindow) {
console.log('⚠️ iframe.contentWindow is null, retrying in 100ms...');
setTimeout(restoreScrollAfterDeviceChange, 100);
return;
}
@@ -909,7 +885,6 @@ const PreviewFrame = ({
if (parsed && typeof parsed.x === 'number' && typeof parsed.y === 'number') {
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
if (!doc || !doc.body || !iframe.contentWindow) {
console.log('⚠️ Device change: Document or contentWindow not ready, retrying...');
setTimeout(restoreScrollAfterDeviceChange, 100);
return;
}
@@ -920,21 +895,10 @@ const PreviewFrame = ({
const safeX = Math.min(parsed.x, maxScrollX);
const safeY = Math.min(parsed.y, maxScrollY);
console.log('📍 Device change scroll debug:', {
stored: { x: parsed.x, y: parsed.y },
safe: { x: safeX, y: safeY },
contentHeight: doc.body.scrollHeight,
viewportHeight: iframe.contentWindow.innerHeight,
maxScrollY
});
iframe.contentWindow.scrollTo(safeX, safeY);
console.log(`✅ Device change: Scroll restored to ${safeX}, ${safeY}`);
} else {
console.log('⚠️ Invalid scroll data:', parsed);
}
} catch (e) {
console.error('❌ Device change scroll restoration error:', e);
// Silently handle error
}
};
@@ -942,10 +906,7 @@ const PreviewFrame = ({
setTimeout(restoreScrollAfterDeviceChange, 100);
setTimeout(restoreScrollAfterDeviceChange, 300);
setTimeout(restoreScrollAfterDeviceChange, 500);
console.log('📱 Device change: Fresh reload with clean content completed');
} else {
console.log('📝 Code change detected - reload with scroll preservation');
currentBody.innerHTML = tempDiv.innerHTML;
@@ -953,15 +914,12 @@ const PreviewFrame = ({
const restoreScrollToElement = () => {
try {
const storedScroll = localStorage.getItem('htmlPreview_scrollPosition');
console.log('📍 Code change: Attempting element-based scroll restoration...', { storedScroll });
if (!storedScroll) {
console.log('⚠️ Code change: No stored scroll data found');
return;
}
if (!iframe.contentWindow) {
console.log('⚠️ Code change: iframe.contentWindow not ready, retrying...');
setTimeout(restoreScrollToElement, 50);
return;
}
@@ -970,7 +928,6 @@ const PreviewFrame = ({
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
if (!doc || !doc.body || !iframe.contentWindow) {
console.log('⚠️ Code change: Document or contentWindow not ready, retrying...');
setTimeout(restoreScrollToElement, 50);
return;
}
@@ -980,10 +937,8 @@ const PreviewFrame = ({
const targetElement = doc.querySelector(parsed.elementSelector);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'instant', block: 'start' });
console.log(`✅ Code change: Scrolled to element ${parsed.elementSelector}`);
return;
} else {
console.log(`⚠️ Code change: Element ${parsed.elementSelector} not found, trying text match...`);
// Fallback: find element by text content
if (parsed.elementText) {
@@ -991,7 +946,6 @@ const PreviewFrame = ({
for (const el of allElements) {
if (el.textContent && el.textContent.includes(parsed.elementText.substring(0, 20))) {
el.scrollIntoView({ behavior: 'instant', block: 'start' });
console.log(`✅ Code change: Scrolled to element by text match`);
return;
}
}
@@ -1009,12 +963,11 @@ const PreviewFrame = ({
if (safeX > 0 || safeY > 0) {
iframe.contentWindow.scrollTo(safeX, safeY);
console.log(`✅ Code change: Fallback coordinate scroll to ${safeX}, ${safeY}`);
}
}
} catch (e) {
console.error('❌ Code change: Element-based scroll restoration error:', e);
// Silently handle error
}
};
@@ -1022,34 +975,31 @@ const PreviewFrame = ({
setTimeout(restoreScrollToElement, 50);
setTimeout(restoreScrollToElement, 150);
}
// Update head styles
const styleMatch = newContent.match(/<style[^>]*>([\s\S]*?)<\/style>/gi);
if (styleMatch) {
const oldStyles = doc.querySelectorAll('style:not(#inspect-mode-styles):not(#device-frame-styles)');
oldStyles.forEach(style => style.remove());
styleMatch.forEach(styleTag => {
const styleEl = doc.createElement('style');
styleEl.textContent = styleTag.replace(/<\/?style[^>]*>/gi, '');
doc.head.appendChild(styleEl);
});
// Update head styles
const styleMatch = newContent.match(/<style[^>]*>([\s\S]*?)<\/style>/gi);
if (styleMatch) {
const oldStyles = doc.querySelectorAll('style:not(#inspect-mode-styles):not(#device-frame-styles)');
oldStyles.forEach(style => style.remove());
styleMatch.forEach(styleTag => {
const styleEl = doc.createElement('style');
styleEl.textContent = styleTag.replace(/<\/?style[^>]*>/gi, '');
doc.head.appendChild(styleEl);
});
}
}
console.log('🚀 Full reload complete');
}
}
} catch (error) {
console.error('Error updating iframe content:', error);
// Fallback to document.write if selective update fails
try {
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
doc.open();
doc.write(newContent);
doc.close();
console.log('🔄 Fallback to full rewrite');
} catch (fallbackError) {
console.error('Fallback update also failed:', fallbackError);
// Fallback update failed
}
} finally {
isLoadingRef.current = false;
@@ -1074,10 +1024,8 @@ const PreviewFrame = ({
if (!doc || !doc.body) return;
if (inspectMode) {
console.log('🎨 Applying inspect mode styles.');
setupInspectModeStyles(doc);
} else {
console.log('🧹 Removing inspect mode styles.');
// Remove inspect mode styles when disabled
const existingStyle = doc.getElementById('inspect-mode-styles');
if (existingStyle) {
@@ -1104,7 +1052,6 @@ const PreviewFrame = ({
const currentX = win.pageXOffset || win.scrollX || 0;
const currentY = win.pageYOffset || win.scrollY || 0;
console.log(`📊 TRACKING SCROLL: Current position (${currentX}, ${currentY})`);
scrollPositionRef.current = { x: currentX, y: currentY };
// Store scroll position with element-based tracking
@@ -1113,7 +1060,6 @@ const PreviewFrame = ({
try {
// Store in PARENT window localStorage, not iframe localStorage
window.localStorage.setItem('htmlPreview_scrollPosition', JSON.stringify(scrollData));
console.log(`💾 STORED SCROLL: Saved position (${currentX}, ${currentY}) to localStorage`);
} catch (e) {
// Silently handle localStorage errors
}
@@ -1191,11 +1137,9 @@ const PreviewFrame = ({
}, []);
const getDeviceWrapper = () => {
console.log('🔧 Device Frame Debug:', { isFullscreen, selectedDevice });
// Non-fullscreen always uses iPhone frame (mobile view)
if (!isFullscreen) {
console.log('📱 Non-fullscreen: Using iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
@@ -1204,7 +1148,6 @@ const PreviewFrame = ({
// Fullscreen desktop mode: no frame
if (selectedDevice === 'desktop') {
console.log('🖥️ Desktop fullscreen: No device frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
@@ -1213,19 +1156,16 @@ const PreviewFrame = ({
switch (selectedDevice) {
case 'tablet':
console.log('📟 Rendering iPad Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'ipad-pro'
};
case 'mobile':
console.log('📱 Rendering iPhone 14 Pro frame');
return {
wrapperClass: 'flex justify-center items-center w-full h-full',
deviceFrame: 'iphone-14-pro'
};
default:
console.log('❓ Unknown device, no frame');
return {
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
deviceFrame: null
@@ -1242,7 +1182,6 @@ const PreviewFrame = ({
if (deviceFrame) {
// Render with device frame (iPhone 14 Pro or iPad Pro)
console.log(`🎨 Rendering device frame: device-${deviceFrame}`);
return (
<div className={wrapperClass}>
<div className={`device device-${deviceFrame}`}>

View File

@@ -28,7 +28,6 @@ const Toolbar = ({
cleanupInspectorState();
} else {
// Activate inspect mode (no need to cleanup when activating)
console.log('🎯 TOOLBAR: Activating inspect mode');
setInspectMode(true);
}
};

View File

@@ -1,8 +1,10 @@
// Google Analytics utility for React SPA
// Implements best practices for Single Page Applications
import { initConsentMode, applyStoredConsent } from './consentManager';
// Google Analytics configuration
const GA_MEASUREMENT_ID = 'G-S3K5P2PWV6';
const GA_MEASUREMENT_ID = process.env.REACT_APP_GA_ID || 'G-S3K5P2PWV6';
// Initialize Google Analytics with Consent Mode v2
export const initGA = () => {
@@ -22,7 +24,6 @@ export const initGA = () => {
window.gtag = gtag;
// Initialize Consent Mode v2 BEFORE loading GA script
const { initConsentMode, applyStoredConsent } = require('./consentManager');
initConsentMode();
// Create script elements

View File

@@ -122,18 +122,14 @@ export const initBrowserCompat = () => {
// Add specific fixes for Telegram browser
if (browserInfo.isTelegram) {
console.log('Telegram browser detected - applying compatibility fixes');
// Add Telegram-specific error handling
window.addEventListener('error', (event) => {
console.log('Global error caught in Telegram browser:', event.error);
// Prevent the error from bubbling up and showing the error overlay
event.preventDefault();
return true;
});
window.addEventListener('unhandledrejection', (event) => {
console.log('Unhandled promise rejection in Telegram browser:', event.reason);
// Prevent the error from bubbling up
event.preventDefault();
return true;

View File

@@ -185,7 +185,7 @@ export const getConsentBannerData = () => {
{
id: CONSENT_CATEGORIES.ADVERTISING,
name: 'Advertising',
description: 'Future ad personalization (not yet implemented)',
description: 'Ad personalization and targeting (Adsterra)',
required: false
}
],

View File

@@ -79,7 +79,7 @@ export const fetchUrlContent = async (url) => {
}
}
} catch (directError) {
console.log('Direct fetch failed, trying CORS proxy:', directError.message);
// Direct fetch failed, trying CORS proxy
}
// Try CORS proxies
@@ -124,7 +124,6 @@ export const fetchUrlContent = async (url) => {
}
} catch (proxyError) {
lastError = proxyError;
console.log(`Proxy ${proxy} failed:`, proxyError.message);
continue;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -44,7 +44,6 @@ export const fetchGitHubReleases = async (owner, repo, token = null) => {
url: commit.html_url
}));
} catch (error) {
console.error('Failed to fetch GitHub releases:', error);
return [];
}
};
@@ -79,7 +78,6 @@ export const fetchGiteaReleases = async (owner, repo, token, baseUrl) => {
url: `${baseUrl}/${owner}/${repo}/commit/${commit.sha}`
}));
} catch (error) {
console.error('Failed to fetch Gitea releases:', error);
return [];
}
};
@@ -95,7 +93,6 @@ export const fetchCustomReleases = async (apiEndpoint) => {
return await response.json();
} catch (error) {
console.error('Failed to fetch custom releases:', error);
return [];
}
};
@@ -111,7 +108,6 @@ export const fetchStaticReleases = async () => {
return await response.json();
} catch (error) {
console.error('Failed to fetch static releases:', error);
return [];
}
};

View File

@@ -248,7 +248,7 @@ export const getCoreWebVitalsOptimizations = () => {
// Cumulative Layout Shift (CLS)
cls: {
setImageDimensions: true,
reserveSpaceForAds: true, // Important for future AdSense
reserveSpaceForAds: true,
avoidDynamicContent: true,
useTransforms: true
}

View File

@@ -1,27 +0,0 @@
{/* Export Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
<div className="flex min-w-max">
<button
onClick={() => setActiveExportTab('json')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
activeExportTab === 'json'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Braces className="h-4 w-4" />
JSON
</button>
<button
onClick={() => setActiveExportTab('php')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
activeExportTab === 'php'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Code className="h-4 w-4" />
PHP
</button>
</div>
</div>