feat: Adsterra integration, code splitting, cleanup, Onidel affiliate
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Google Analytics Measurement ID
|
||||||
|
REACT_APP_GA_ID=G-S3K5P2PWV6
|
||||||
@@ -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.
|
|
||||||
@@ -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!** 🎉
|
|
||||||
@@ -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
201
README.md
@@ -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
|
## 🚀 Features
|
||||||
|
|
||||||
### Available Tools
|
### Available Tools
|
||||||
|
|
||||||
1. **JSON Encoder/Decoder** - Format, validate, and minify JSON data with syntax highlighting
|
1. **Object Editor** (`/object-editor`) — Visual editor for JSON and PHP serialized objects with mindmap visualization.
|
||||||
2. **Serialize Encoder/Decoder** - Encode and decode PHP serialized data
|
2. **Table Editor** (`/table-editor`) — Import, edit, and export tabular data from URLs, files, or paste CSV/JSON.
|
||||||
3. **URL Encoder/Decoder** - Encode and decode URLs and query parameters
|
3. **Markdown Editor** (`/markdown-editor`) — Write and preview markdown with live rendering, syntax highlighting, and export.
|
||||||
4. **Base64 Encoder/Decoder** - Convert text to Base64 and back with file support
|
4. **Invoice Editor** (`/invoice-editor`) — Create, edit, and export professional invoices with PDF generation.
|
||||||
5. **CSV ↔ JSON Converter** - Convert between CSV and JSON formats with custom delimiters
|
5. **URL Encoder/Decoder** (`/url`) — Encode and decode URLs and query parameters.
|
||||||
6. **Code Beautifier/Minifier** - Format and minify JSON, XML, SQL, CSS, and HTML code
|
6. **Base64 Encoder/Decoder** (`/base64`) — Convert text to Base64 and back with file support.
|
||||||
7. **Text Diff Checker** - Compare two texts and highlight differences line by line
|
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
|
### Key Features
|
||||||
|
|
||||||
- ✨ **Modern UI** - Clean, minimalist design with automatic dark/light mode
|
- ✨ **Modern UI** — Clean, minimalist design with automatic dark/light mode.
|
||||||
- 📱 **Fully Responsive** - Equal experience on desktop and mobile devices
|
- 📱 **Fully Responsive** — Equal experience on desktop and mobile devices.
|
||||||
- ⚡ **Lightning Fast** - All processing happens locally in the browser
|
- ⚡ **Lightning Fast** — Code-splitting with React.lazy for optimal loading performance.
|
||||||
- 🔒 **Privacy First** - No server dependencies, no data collection
|
- 🔒 **Privacy First** — No server dependencies, no data collection.
|
||||||
- 📋 **Easy Copy-Paste** - One-click copy functionality for all outputs
|
- 📋 **Easy Copy-Paste** — One-click copy functionality for all outputs.
|
||||||
- 📁 **File Support** - Upload files directly for processing
|
- 📁 **File Support** — Upload files directly for processing.
|
||||||
- 🔍 **Searchable** - Quick search through available tools
|
- 🔍 **Searchable** — Quick search through available tools.
|
||||||
|
- 🎯 **SEO Optimized** — Pre-rendered pages with React Snap, meta tags, and sitemap.
|
||||||
|
|
||||||
## 🛠 Tech Stack
|
## 🛠 Tech Stack
|
||||||
|
|
||||||
- **Frontend**: React 18, React Router
|
- **Frontend**: React 18, React Router 6
|
||||||
- **Styling**: TailwindCSS with custom design system
|
- **Styling**: TailwindCSS 3
|
||||||
|
- **Editor**: CodeMirror 6 (@uiw/react-codemirror)
|
||||||
|
- **PDF Generation**: jsPDF, jsPDF-AutoTable
|
||||||
|
- **Markdown**: marked, DOMPurify, highlight.js
|
||||||
- **Icons**: Lucide React
|
- **Icons**: Lucide React
|
||||||
- **Build Tool**: Create React App
|
- **SEO**: react-helmet-async
|
||||||
- **Libraries**:
|
- **Analytics**: Google Analytics 4 with Consent Mode v2
|
||||||
- js-beautify (code formatting)
|
- **Advertising**: Adsterra
|
||||||
|
- **Utilities**:
|
||||||
- papaparse (CSV parsing)
|
- papaparse (CSV parsing)
|
||||||
|
- js-beautify (code formatting)
|
||||||
- serialize-javascript (serialization)
|
- serialize-javascript (serialization)
|
||||||
|
- diff-match-patch (text comparison)
|
||||||
|
- turndown (HTML to Markdown)
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
cd developer-tools
|
cd developer-tools
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install dependencies:
|
2. Install dependencies:
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Start the development server:
|
3. Configure environment (optional):
|
||||||
```bash
|
```bash
|
||||||
npm start
|
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
|
## 🏗 Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── components/ # Reusable UI components
|
├── components/ # Reusable UI components
|
||||||
│ ├── Layout.js # Main layout wrapper
|
│ ├── Layout.js # Main layout with sidebar navigation
|
||||||
│ ├── ToolCard.js # Tool card component for home page
|
│ ├── ToolLayout.js # Wrapper for tool pages with ad slots
|
||||||
│ ├── ToolLayout.js # Layout for individual tools
|
│ ├── ToolCard.js # Tool card for homepage
|
||||||
│ └── CopyButton.js # Copy to clipboard button
|
│ ├── ToolSidebar.js # Sidebar navigation
|
||||||
├── pages/ # Individual tool pages
|
│ ├── AdBlock.js # Adsterra ad unit wrapper
|
||||||
│ ├── Home.js # Homepage with tool listing
|
│ ├── AdColumn.js # Desktop sidebar ad column
|
||||||
│ ├── JsonTool.js # JSON encoder/decoder
|
│ ├── MobileAdBanner.js # Mobile bottom ad banner
|
||||||
│ ├── SerializeTool.js # Serialize encoder/decoder
|
│ ├── OfferBlock.js # Promotional offer slot
|
||||||
│ ├── UrlTool.js # URL encoder/decoder
|
│ ├── AffiliateBlock.js # Affiliate link slot
|
||||||
│ ├── Base64Tool.js # Base64 encoder/decoder
|
│ ├── Loading.js # Loading spinner for Suspense
|
||||||
│ ├── CsvJsonTool.js # CSV/JSON converter
|
│ ├── CodeMirrorEditor.js # Code editor component
|
||||||
│ ├── BeautifierTool.js# Code beautifier/minifier
|
│ ├── CodeEditor.js # Alternative code editor
|
||||||
│ └── DiffTool.js # Text diff checker
|
│ ├── SEO.js / SEOHead.js # SEO meta tags
|
||||||
├── App.js # Main app component with routing
|
│ ├── RelatedTools.js # Related tools suggestions
|
||||||
├── index.js # React app entry point
|
│ ├── ErrorBoundary.js # Error boundary
|
||||||
└── index.css # Global styles and Tailwind imports
|
│ ├── 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
|
## 🚀 Available Scripts
|
||||||
|
|
||||||
- `npm start` - Runs the app in development mode
|
- `npm run dev` or `npm start` — Runs the app in development mode.
|
||||||
- `npm build` - Builds the app for production
|
- `npm run build` — Builds the app for production (with React Snap for pre-rendering).
|
||||||
- `npm test` - Launches the test runner
|
- `npm run build:no-snap` — Builds the app for production (without pre-rendering).
|
||||||
- `npm run eject` - Ejects from Create React App (one-way operation)
|
- `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
|
| Variable | Description | Default |
|
||||||
2. **Individual Tools**: Each tool has a dedicated page with:
|
|----------|-------------|---------|
|
||||||
- Input area for your data
|
| `REACT_APP_GA_ID` | Google Analytics Measurement ID | `G-S3K5P2PWV6` |
|
||||||
- Processing controls (encode/decode, beautify/minify, etc.)
|
|
||||||
- Output area with copy functionality
|
|
||||||
- File upload support where applicable
|
|
||||||
- Usage tips and examples
|
|
||||||
|
|
||||||
## 🔧 Customization
|
Create a `.env` file based on `.env.example` to override defaults.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
## 🌟 Contributing
|
## 🌟 Contributing
|
||||||
|
|
||||||
@@ -123,13 +158,3 @@ The application is built with scalability in mind:
|
|||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
This project is open source and available under the MIT 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
|
|
||||||
|
|||||||
@@ -19,12 +19,10 @@
|
|||||||
|
|
||||||
### 1. **Add Missing Tools to Sitemap**
|
### 1. **Add Missing Tools to Sitemap**
|
||||||
|
|
||||||
**Current Issue:** Sitemap is missing several tools:
|
**Current Issue:** Sitemap was missing the Markdown Editor and Release Notes pages.
|
||||||
- ❌ Markdown Editor
|
|
||||||
- ❌ JSON Tool
|
> **Note:** `/json`, `/csv-json`, and `/serialize` routes were planned but never implemented.
|
||||||
- ❌ CSV/JSON Converter
|
> These tools are now consolidated into the Object Editor (`/object-editor`).
|
||||||
- ❌ Serialize Tool
|
|
||||||
- ❌ Release Notes page
|
|
||||||
|
|
||||||
**Action:** Update `public/sitemap.xml`
|
**Action:** Update `public/sitemap.xml`
|
||||||
|
|
||||||
@@ -36,24 +34,6 @@
|
|||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</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>
|
<url>
|
||||||
<loc>https://dewe.dev/release-notes</loc>
|
<loc>https://dewe.dev/release-notes</loc>
|
||||||
<lastmod>2025-10-22</lastmod>
|
<lastmod>2025-10-22</lastmod>
|
||||||
@@ -296,8 +276,8 @@ Write and preview markdown with live rendering
|
|||||||
|
|
||||||
**Add to `index.html`:**
|
**Add to `index.html`:**
|
||||||
```html
|
```html
|
||||||
<link rel="preconnect" href="https://pagead2.googlesyndication.com">
|
<link rel="preconnect" href="https://bustleplaguereed.com">
|
||||||
<link rel="dns-prefetch" href="https://pagead2.googlesyndication.com">
|
<link rel="dns-prefetch" href="https://bustleplaguereed.com">
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -45,7 +45,6 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-router-dom": "6.26.2",
|
"react-router-dom": "6.26.2",
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"react-snap": "^1.23.0",
|
"react-snap": "^1.23.0",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"serialize-javascript": "^6.0.0",
|
"serialize-javascript": "^6.0.0",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"description": "Web Developer Tools MVP - Utilities Toolkit",
|
"description": "Web Developer Tools MVP - Utilities Toolkit",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/basic-setup": "^0.20.0",
|
|
||||||
"@codemirror/commands": "^6.8.1",
|
"@codemirror/commands": "^6.8.1",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "^6.4.9",
|
||||||
@@ -41,7 +40,6 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-router-dom": "6.26.2",
|
"react-router-dom": "6.26.2",
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"react-snap": "^1.23.0",
|
"react-snap": "^1.23.0",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"serialize-javascript": "^6.0.0",
|
"serialize-javascript": "^6.0.0",
|
||||||
@@ -56,6 +54,7 @@
|
|||||||
"tailwindcss": "^3.3.0"
|
"tailwindcss": "^3.3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "react-scripts start",
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"start:prod": "serve -s build -l 3000",
|
"start:prod": "serve -s build -l 3000",
|
||||||
"build": "react-scripts build && react-snap",
|
"build": "react-scripts build && react-snap",
|
||||||
@@ -83,7 +82,7 @@
|
|||||||
"/beautifier",
|
"/beautifier",
|
||||||
"/diff",
|
"/diff",
|
||||||
"/text-length",
|
"/text-length",
|
||||||
"/whats-new",
|
"/markdown-editor",
|
||||||
"/privacy",
|
"/privacy",
|
||||||
"/terms"
|
"/terms"
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
public/images/onidel-banner.webp
Normal file
BIN
public/images/onidel-banner.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -14,9 +14,7 @@
|
|||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<title>Developer Tools - Web Developer Utilities</title>
|
<title>Developer Tools - Web Developer Utilities</title>
|
||||||
|
|
||||||
<!-- Google AdSense -->
|
<!-- Adsterra -->
|
||||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8644544686212757"
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
@@ -6,6 +6,18 @@
|
|||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
"type": "image/x-icon"
|
"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": ".",
|
"start_url": ".",
|
||||||
|
|||||||
@@ -72,12 +72,6 @@
|
|||||||
</url>
|
</url>
|
||||||
|
|
||||||
<!-- Info Pages -->
|
<!-- Info Pages -->
|
||||||
<url>
|
|
||||||
<loc>https://dewe.dev/whats-new</loc>
|
|
||||||
<lastmod>2025-10-22</lastmod>
|
|
||||||
<changefreq>weekly</changefreq>
|
|
||||||
<priority>0.7</priority>
|
|
||||||
</url>
|
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dewe.dev/release-notes</loc>
|
<loc>https://dewe.dev/release-notes</loc>
|
||||||
<lastmod>2025-10-22</lastmod>
|
<lastmod>2025-10-22</lastmod>
|
||||||
|
|||||||
42
src/App.js
42
src/App.js
@@ -1,28 +1,30 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, Suspense, lazy } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { HelmetProvider } from 'react-helmet-async';
|
import { HelmetProvider } from 'react-helmet-async';
|
||||||
import Layout from './components/Layout';
|
import Layout from './components/Layout';
|
||||||
import ErrorBoundary from './components/ErrorBoundary';
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
import Home from './pages/Home';
|
import Loading from './components/Loading';
|
||||||
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 { initGA } from './utils/analytics';
|
import { initGA } from './utils/analytics';
|
||||||
|
|
||||||
import './index.css';
|
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() {
|
function App() {
|
||||||
// Initialize Google Analytics on app startup
|
// Initialize Google Analytics on app startup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,6 +36,7 @@ function App() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Router>
|
<Router>
|
||||||
<Layout>
|
<Layout>
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/url" element={<UrlTool />} />
|
<Route path="/url" element={<UrlTool />} />
|
||||||
@@ -47,12 +50,13 @@ function App() {
|
|||||||
<Route path="/markdown-editor" element={<MarkdownEditor />} />
|
<Route path="/markdown-editor" element={<MarkdownEditor />} />
|
||||||
<Route path="/invoice-preview" element={<InvoicePreview />} />
|
<Route path="/invoice-preview" element={<InvoicePreview />} />
|
||||||
<Route path="/invoice-preview-minimal" element={<InvoicePreviewMinimal />} />
|
<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="/release-notes" element={<ReleaseNotes />} />
|
||||||
<Route path="/privacy" element={<PrivacyPolicy />} />
|
<Route path="/privacy" element={<PrivacyPolicy />} />
|
||||||
<Route path="/terms" element={<TermsOfService />} />
|
<Route path="/terms" element={<TermsOfService />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Router>
|
</Router>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -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(() => {
|
useEffect(() => {
|
||||||
try {
|
if (!iframeRef.current) return;
|
||||||
// Push ad to AdSense queue
|
|
||||||
(window.adsbygoogle = window.adsbygoogle || []).push({});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('AdSense error:', e);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className={`bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${className}`}>
|
<iframe
|
||||||
<ins
|
ref={iframeRef}
|
||||||
className="adsbygoogle"
|
className={`bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${className}`}
|
||||||
style={{
|
style={{ width: '300px', height: '250px', border: 'none' }}
|
||||||
display: 'block',
|
title="Advertisement"
|
||||||
width: `${width}px`,
|
sandbox="allow-scripts allow-same-origin"
|
||||||
height: `${height}px`
|
/>
|
||||||
}}
|
|
||||||
data-ad-client="ca-pub-8644544686212757"
|
|
||||||
data-ad-slot={slot}
|
|
||||||
data-ad-format="fixed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AdBlock from './AdBlock';
|
import AdBlock from './AdBlock';
|
||||||
|
import OfferBlock from './OfferBlock';
|
||||||
|
import AffiliateBlock from './AffiliateBlock';
|
||||||
|
|
||||||
/**
|
const AdColumn = () => {
|
||||||
* 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'
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<aside className="hidden xl:block w-[300px] flex-shrink-0">
|
<aside className="hidden xl:block w-[300px] flex-shrink-0">
|
||||||
<div className="fixed top-20 right-8 w-[300px] space-y-5">
|
<div className="sticky top-20 space-y-5">
|
||||||
{/* Ad 1: Medium Rectangle */}
|
<AdBlock />
|
||||||
<AdBlock slot={slot1} size="300x250" />
|
<OfferBlock />
|
||||||
|
<AffiliateBlock />
|
||||||
{/* Ad 2: Medium Rectangle */}
|
|
||||||
<AdBlock slot={slot2} size="300x250" />
|
|
||||||
|
|
||||||
{/* Ad 3: Medium Rectangle */}
|
|
||||||
<AdBlock slot={slot3} size="300x250" />
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
21
src/components/AffiliateBlock.js
Normal file
21
src/components/AffiliateBlock.js
Normal 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
18
src/components/Loading.js
Normal 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;
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
/**
|
const MobileAdBanner = () => {
|
||||||
* 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 [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(true);
|
||||||
const [closed, setClosed] = useState(false);
|
const [closed, setClosed] = useState(false);
|
||||||
|
const iframeRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if user previously closed the banner (session storage)
|
|
||||||
const wasClosed = sessionStorage.getItem('mobileAdClosed');
|
const wasClosed = sessionStorage.getItem('mobileAdClosed');
|
||||||
if (wasClosed === 'true') {
|
if (wasClosed === 'true') {
|
||||||
setClosed(true);
|
setClosed(true);
|
||||||
@@ -20,13 +15,41 @@ const MobileAdBanner = ({ slot = 'REPLACE_WITH_MOBILE_SLOT' }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && !closed) {
|
if (!visible || closed || !iframeRef.current) return;
|
||||||
try {
|
|
||||||
(window.adsbygoogle = window.adsbygoogle || []).push({});
|
const timer = setTimeout(() => {
|
||||||
} catch (e) {
|
if (!iframeRef.current) return;
|
||||||
console.error('AdSense error:', e);
|
|
||||||
}
|
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]);
|
}, [visible, closed]);
|
||||||
|
|
||||||
const handleClose = () => {
|
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">
|
<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
|
<button
|
||||||
onClick={handleClose}
|
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"
|
aria-label="Close ad"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<div className="flex justify-center py-2">
|
<div className="flex justify-center items-center py-2">
|
||||||
<ins
|
<iframe
|
||||||
className="adsbygoogle"
|
ref={iframeRef}
|
||||||
style={{
|
style={{ width: '320px', height: '50px', border: 'none' }}
|
||||||
display: 'inline-block',
|
title="Mobile Advertisement"
|
||||||
width: '320px',
|
sandbox="allow-scripts allow-same-origin"
|
||||||
height: '50px'
|
|
||||||
}}
|
|
||||||
data-ad-client="ca-pub-8644544686212757"
|
|
||||||
data-ad-slot={slot}
|
|
||||||
data-ad-format="fixed"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
22
src/components/OfferBlock.js
Normal file
22
src/components/OfferBlock.js
Normal 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;
|
||||||
@@ -111,7 +111,6 @@ const InvoiceEditor = () => {
|
|||||||
const currencyData = await response.json();
|
const currencyData = await response.json();
|
||||||
setCurrencies(currencyData);
|
setCurrencies(currencyData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load currencies:', error);
|
|
||||||
// Fallback to basic currencies
|
// Fallback to basic currencies
|
||||||
setCurrencies([
|
setCurrencies([
|
||||||
{ code: 'IDR', name: 'Indonesian Rupiah', symbol: 'Rp' },
|
{ code: 'IDR', name: 'Indonesian Rupiah', symbol: 'Rp' },
|
||||||
@@ -165,7 +164,7 @@ const InvoiceEditor = () => {
|
|||||||
setPdfPageSize(savedPageSize);
|
setPdfPageSize(savedPageSize);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load saved invoice:', error);
|
// Failed to load saved invoice
|
||||||
}
|
}
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
@@ -176,7 +175,7 @@ const InvoiceEditor = () => {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
|
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save invoice:', error);
|
// Failed to save invoice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [invoiceData, createNewCompleted]);
|
}, [invoiceData, createNewCompleted]);
|
||||||
@@ -227,7 +226,7 @@ const InvoiceEditor = () => {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem('pdfPageSize', pdfPageSize);
|
localStorage.setItem('pdfPageSize', pdfPageSize);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save PDF page size:', error);
|
// Failed to save PDF page size
|
||||||
}
|
}
|
||||||
}, [pdfPageSize]);
|
}, [pdfPageSize]);
|
||||||
|
|
||||||
@@ -619,7 +618,6 @@ const InvoiceEditor = () => {
|
|||||||
// Navigate to preview page
|
// Navigate to preview page
|
||||||
navigate('/invoice-preview');
|
navigate('/invoice-preview');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save invoice data:', error);
|
|
||||||
alert('Failed to save invoice data. Please try again.');
|
alert('Failed to save invoice data. Please try again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ const InvoicePreview = () => {
|
|||||||
setPdfPageSize(savedPageSize);
|
setPdfPageSize(savedPageSize);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load invoice data:', error);
|
|
||||||
navigate('/invoice-editor');
|
navigate('/invoice-editor');
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
@@ -262,7 +261,6 @@ const InvoicePreview = () => {
|
|||||||
|
|
||||||
await html2pdf().set(opt).from(element).save();
|
await html2pdf().set(opt).from(element).save();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PDF generation failed:', error);
|
|
||||||
alert('Failed to generate PDF. Please try again.');
|
alert('Failed to generate PDF. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
// Restore original styles after a short delay
|
// Restore original styles after a short delay
|
||||||
@@ -281,7 +279,7 @@ const InvoicePreview = () => {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
|
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
|
||||||
} catch (error) {
|
} 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
|
// Add a parameter to indicate we're editing existing data
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ const InvoicePreviewMinimal = () => {
|
|||||||
setPdfPageSize(savedPageSize);
|
setPdfPageSize(savedPageSize);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load invoice data:', error);
|
|
||||||
navigate('/invoice-editor');
|
navigate('/invoice-editor');
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
@@ -164,7 +163,6 @@ const InvoicePreviewMinimal = () => {
|
|||||||
|
|
||||||
await html2pdf().set(opt).from(element).save();
|
await html2pdf().set(opt).from(element).save();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PDF generation failed:', error);
|
|
||||||
alert('Failed to generate PDF. Please try again.');
|
alert('Failed to generate PDF. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
// Restore original styles after a short delay
|
// Restore original styles after a short delay
|
||||||
|
|||||||
@@ -58,8 +58,6 @@ const MarkdownEditor = () => {
|
|||||||
const language = token.lang || '';
|
const language = token.lang || '';
|
||||||
const normalizedLang = language ? language.toLowerCase().trim() : '';
|
const normalizedLang = language ? language.toLowerCase().trim() : '';
|
||||||
|
|
||||||
console.log('Code block detected:', { language, normalizedLang, codeLength: codeString.length });
|
|
||||||
|
|
||||||
let highlightedCode = codeString;
|
let highlightedCode = codeString;
|
||||||
|
|
||||||
// Apply syntax highlighting
|
// Apply syntax highlighting
|
||||||
@@ -67,18 +65,14 @@ const MarkdownEditor = () => {
|
|||||||
try {
|
try {
|
||||||
const result = hljs.highlight(codeString, { language: normalizedLang });
|
const result = hljs.highlight(codeString, { language: normalizedLang });
|
||||||
highlightedCode = result.value;
|
highlightedCode = result.value;
|
||||||
console.log('Highlighted with language:', normalizedLang);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Highlight error for ${normalizedLang}:`, e);
|
|
||||||
highlightedCode = codeString;
|
highlightedCode = codeString;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const result = hljs.highlightAuto(codeString);
|
const result = hljs.highlightAuto(codeString);
|
||||||
highlightedCode = result.value;
|
highlightedCode = result.value;
|
||||||
console.log('Auto-highlighted, detected:', result.language);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Auto-highlight error:', e);
|
|
||||||
highlightedCode = codeString;
|
highlightedCode = codeString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +130,6 @@ const MarkdownEditor = () => {
|
|||||||
ADD_ATTR: ['data-code-id', 'title', 'id', 'type', 'checked', 'disabled']
|
ADD_ATTR: ['data-code-id', 'title', 'id', 'type', 'checked', 'disabled']
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Markdown parse error:', e);
|
|
||||||
return '<p>Error parsing markdown</p>';
|
return '<p>Error parsing markdown</p>';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -173,7 +166,7 @@ const MarkdownEditor = () => {
|
|||||||
button.textContent = originalText;
|
button.textContent = originalText;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('Failed to copy:', err);
|
// Failed to copy
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -818,7 +811,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
response = await fetch(urlToFetch);
|
response = await fetch(urlToFetch);
|
||||||
} catch (corsError) {
|
} catch (corsError) {
|
||||||
// If CORS error, try with CORS proxy
|
// 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)}`);
|
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) {
|
} catch (err) {
|
||||||
console.error('Fetch error:', err);
|
|
||||||
setError(`Failed to fetch from URL: ${err.message}`);
|
setError(`Failed to fetch from URL: ${err.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setFetching(false);
|
setFetching(false);
|
||||||
@@ -1494,7 +1485,6 @@ ${html}
|
|||||||
html2pdf().set(opt).from(wrapper).save().then(() => {
|
html2pdf().set(opt).from(wrapper).save().then(() => {
|
||||||
setError('');
|
setError('');
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error('PDF generation error:', err);
|
|
||||||
setError('Failed to generate PDF');
|
setError('Failed to generate PDF');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -385,9 +385,6 @@ const ObjectEditor = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = parseValue();
|
const result = parseValue();
|
||||||
if (index < str.length) {
|
|
||||||
console.warn(`Warning: Trailing data after parsing: "${str.substring(index)}"`);
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Parse error at position ${index}: ${error.message}`);
|
throw new Error(`Parse error at position ${index}: ${error.message}`);
|
||||||
@@ -563,7 +560,6 @@ const ObjectEditor = () => {
|
|||||||
serialized
|
serialized
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating outputs:', error);
|
|
||||||
setOutputs({
|
setOutputs({
|
||||||
jsonPretty: 'Error generating JSON',
|
jsonPretty: 'Error generating JSON',
|
||||||
jsonMinified: 'Error generating JSON',
|
jsonMinified: 'Error generating JSON',
|
||||||
@@ -610,8 +606,6 @@ const ObjectEditor = () => {
|
|||||||
|
|
||||||
// Fetch data from URL with advanced content extraction
|
// Fetch data from URL with advanced content extraction
|
||||||
const handleFetchData = async (advancedOptions = null) => {
|
const handleFetchData = async (advancedOptions = null) => {
|
||||||
console.log('🚀 handleFetchData called with URL:', fetchUrl);
|
|
||||||
console.log('🔧 Advanced options:', advancedOptions);
|
|
||||||
|
|
||||||
const urlToFetch = advancedOptions?.url || fetchUrl.trim();
|
const urlToFetch = advancedOptions?.url || fetchUrl.trim();
|
||||||
|
|
||||||
@@ -623,7 +617,6 @@ const ObjectEditor = () => {
|
|||||||
setFetching(true);
|
setFetching(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
console.log('✅ Starting fetch process...');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Add protocol if missing
|
// Add protocol if missing
|
||||||
@@ -635,11 +628,10 @@ const ObjectEditor = () => {
|
|||||||
// Determine if this is an advanced request (has custom options)
|
// Determine if this is an advanced request (has custom options)
|
||||||
const isAdvancedRequest = advancedOptions && (
|
const isAdvancedRequest = advancedOptions && (
|
||||||
advancedOptions.method !== 'GET' ||
|
advancedOptions.method !== 'GET' ||
|
||||||
Object.keys(advancedOptions.headers || {}).length > 0 ||
|
Object.keys( advancedOptions.headers || {}).length > 0 ||
|
||||||
advancedOptions.body
|
advancedOptions.body
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('🎯 Is advanced request:', isAdvancedRequest);
|
|
||||||
|
|
||||||
// Build fetch options for advanced mode
|
// Build fetch options for advanced mode
|
||||||
const fetchOptions = advancedOptions ? {
|
const fetchOptions = advancedOptions ? {
|
||||||
@@ -648,7 +640,6 @@ const ObjectEditor = () => {
|
|||||||
body: advancedOptions.body || undefined
|
body: advancedOptions.body || undefined
|
||||||
} : {};
|
} : {};
|
||||||
|
|
||||||
console.log('📡 Fetch options:', fetchOptions);
|
|
||||||
|
|
||||||
// Try direct fetch first (for APIs)
|
// Try direct fetch first (for APIs)
|
||||||
try {
|
try {
|
||||||
@@ -700,7 +691,6 @@ const ObjectEditor = () => {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
// CORS error or network error, will try with content extractor for simple GET
|
// 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
|
// Use advanced content extractor ONLY for simple GET requests to HTML pages
|
||||||
@@ -747,7 +737,6 @@ const ObjectEditor = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Fetch error:', err);
|
|
||||||
setError(`Failed to fetch data: ${err.message}`);
|
setError(`Failed to fetch data: ${err.message}`);
|
||||||
setUrlDataSummary(null);
|
setUrlDataSummary(null);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -167,20 +167,19 @@ const PrivacyPolicy = () => {
|
|||||||
<section className="mb-8">
|
<section className="mb-8">
|
||||||
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
|
<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" />
|
<Globe className="h-5 w-5 text-teal-600" />
|
||||||
5. Future Advertising (Google AdSense)
|
5. Advertising (Adsterra)
|
||||||
</h2>
|
</h2>
|
||||||
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4 mb-4">
|
<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">
|
<h3 className="font-semibold text-amber-800 dark:text-amber-200 mb-2">
|
||||||
🔮 Planned Implementation:
|
Advertising:
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-amber-700 dark:text-amber-300 text-sm leading-relaxed mb-3">
|
<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>
|
</p>
|
||||||
<ul className="list-disc list-inside text-amber-700 dark:text-amber-300 space-y-1 text-sm">
|
<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>Ads will be clearly marked and non-intrusive</li>
|
||||||
<li>No impact on tool functionality or performance</li>
|
<li>No impact on tool functionality or performance</li>
|
||||||
<li>Google may use cookies for ad personalization</li>
|
<li>The ad network may use cookies for ad delivery</li>
|
||||||
<li>You can opt-out of personalized ads via Google settings</li>
|
|
||||||
<li><strong>We will NEVER share your tool usage data with advertisers</strong></li>
|
<li><strong>We will NEVER share your tool usage data with advertisers</strong></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,9 +217,9 @@ const PrivacyPolicy = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-l-4 border-green-500 pl-4">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ const TermsOfService = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import { Copy } from 'lucide-react';
|
import { Copy } from 'lucide-react';
|
||||||
|
|
||||||
const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameRef, selectedElementInfo }) => {
|
const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameRef, selectedElementInfo }) => {
|
||||||
console.log('🔍 ELEMENT EDITOR: Received props:', { selectedElementInfo, previewFrameRef: !!previewFrameRef });
|
|
||||||
const [edited, setEdited] = useState(null);
|
const [edited, setEdited] = useState(null);
|
||||||
const textareaRefs = useRef({});
|
const textareaRefs = useRef({});
|
||||||
|
|
||||||
@@ -26,7 +25,6 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
|
|||||||
});
|
});
|
||||||
|
|
||||||
setEdited(elementInfo);
|
setEdited(elementInfo);
|
||||||
console.log('🎯 ENHANCED EDITOR: Initialized with selected element:', elementInfo);
|
|
||||||
} else {
|
} else {
|
||||||
// Clear the editor when no element is selected
|
// Clear the editor when no element is selected
|
||||||
setEdited(null);
|
setEdited(null);
|
||||||
@@ -62,7 +60,6 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
|
|||||||
|
|
||||||
// ENHANCED OPTION A: Field change handler using PreviewFrame API
|
// ENHANCED OPTION A: Field change handler using PreviewFrame API
|
||||||
const handleFieldChange = (field, value) => {
|
const handleFieldChange = (field, value) => {
|
||||||
console.log(`⌨️ ENHANCED: Field '${field}' changed to '${value}'`);
|
|
||||||
setEdited(prev => ({ ...prev, [field]: value }));
|
setEdited(prev => ({ ...prev, [field]: value }));
|
||||||
|
|
||||||
// Use Enhanced Option A API for direct DOM manipulation
|
// 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);
|
success = previewFrameRef.current.updateElementStyle(edited.cascadeId, field, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (!success) {
|
||||||
console.log(`✅ ENHANCED UPDATE: ${field} updated in iframe DOM (scroll preserved)`);
|
// Silently handle failure
|
||||||
} else {
|
|
||||||
console.error(`❌ ENHANCED UPDATE: Failed to update ${field}`);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn('⚠️ ENHANCED UPDATE: PreviewFrame ref or cascadeId not available');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
console.log('💾 ENHANCED OPTION A SAVE: Using PreviewFrame API to get iframe content');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use Enhanced Option A API to get iframe content
|
// Use Enhanced Option A API to get iframe content
|
||||||
if (previewFrameRef?.current?.getIframeContent) {
|
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
|
// ENHANCED OPTION A: Update htmlInput with the cleaned iframe content
|
||||||
// This is the ONLY allowed setHtmlInput call during inspector operations (explicit save)
|
// This is the ONLY allowed setHtmlInput call during inspector operations (explicit save)
|
||||||
setHtmlInput(cleanedHtml);
|
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) {
|
} catch (error) {
|
||||||
console.error('❌ Enhanced Option A Save failed:', error);
|
// Silently handle error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger parent save callback
|
// 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
|
// 5. ENHANCED OPTION A: Cancel reverts changes in iframe DOM directly
|
||||||
const handleCancel = () => {
|
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
|
// For Enhanced Option A, we don't need to revert HTML input
|
||||||
// The iframe DOM changes will be discarded when the inspector closes
|
// The iframe DOM changes will be discarded when the inspector closes
|
||||||
// Just close the inspector without syncing changes
|
// Just close the inspector without syncing changes
|
||||||
console.log('✅ ENHANCED CANCEL: Changes discarded, iframe DOM remains stable');
|
|
||||||
onClose();
|
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}>`;
|
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);
|
await navigator.clipboard.writeText(elementString);
|
||||||
console.log('✅ Element copied to clipboard');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ Failed to copy element:', err);
|
// Silently handle error
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -345,13 +345,11 @@ const PreviewFrame = ({
|
|||||||
|
|
||||||
// Mark new selection
|
// Mark new selection
|
||||||
const clickedElement = e.target;
|
const clickedElement = e.target;
|
||||||
console.log(`🎯 ELEMENT SELECT: Clicked on <${clickedElement.tagName.toLowerCase()}> element`);
|
|
||||||
clickedElement.setAttribute('data-selected', 'true');
|
clickedElement.setAttribute('data-selected', 'true');
|
||||||
|
|
||||||
// Assign unique cascade-id for inspector operations
|
// Assign unique cascade-id for inspector operations
|
||||||
const cascadeId = `cascade-${Date.now()}`;
|
const cascadeId = `cascade-${Date.now()}`;
|
||||||
clickedElement.setAttribute('data-cascade-id', cascadeId);
|
clickedElement.setAttribute('data-cascade-id', cascadeId);
|
||||||
console.log(`🏷️ ELEMENT SELECT: Assigned cascade-id: ${cascadeId}`);
|
|
||||||
|
|
||||||
const elementInfo = {
|
const elementInfo = {
|
||||||
tagName: clickedElement.tagName.toLowerCase(),
|
tagName: clickedElement.tagName.toLowerCase(),
|
||||||
@@ -372,9 +370,8 @@ const PreviewFrame = ({
|
|||||||
} else {
|
} else {
|
||||||
window.currentIframeDom = updatedHtml;
|
window.currentIframeDom = updatedHtml;
|
||||||
}
|
}
|
||||||
console.log('💾 ELEMENT SELECT: Stored current iframe DOM with cascade-id');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('⚠️ Could not store iframe DOM:', error);
|
// silently handle iframe DOM storage failures
|
||||||
}
|
}
|
||||||
|
|
||||||
onElementClick(elementInfo);
|
onElementClick(elementInfo);
|
||||||
@@ -382,7 +379,6 @@ const PreviewFrame = ({
|
|||||||
|
|
||||||
// Function to setup inspect mode styles and event handlers with MutationObserver
|
// Function to setup inspect mode styles and event handlers with MutationObserver
|
||||||
const setupInspectModeStyles = useCallback((iframeDoc) => {
|
const setupInspectModeStyles = useCallback((iframeDoc) => {
|
||||||
console.log('🎨 PreviewFrame: Setting up robust inspect mode with MutationObserver');
|
|
||||||
|
|
||||||
// Remove existing inspect styles and observers
|
// Remove existing inspect styles and observers
|
||||||
const existingStyle = iframeDoc.getElementById('inspect-mode-styles');
|
const existingStyle = iframeDoc.getElementById('inspect-mode-styles');
|
||||||
@@ -485,7 +481,6 @@ const PreviewFrame = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (needsReapply) {
|
if (needsReapply) {
|
||||||
console.log('🔄 PreviewFrame: DOM changed, reapplying inspect styles');
|
|
||||||
// Reapply styles after a short delay to ensure new elements are rendered
|
// Reapply styles after a short delay to ensure new elements are rendered
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const currentStyle = iframeDoc.getElementById('inspect-mode-styles');
|
const currentStyle = iframeDoc.getElementById('inspect-mode-styles');
|
||||||
@@ -509,8 +504,6 @@ const PreviewFrame = ({
|
|||||||
|
|
||||||
// Store observer for cleanup
|
// Store observer for cleanup
|
||||||
iframeDoc._inspectObserver = observer;
|
iframeDoc._inspectObserver = observer;
|
||||||
|
|
||||||
console.log('✅ PreviewFrame: Robust inspect mode with MutationObserver applied');
|
|
||||||
}, [handleIframeClick]);
|
}, [handleIframeClick]);
|
||||||
|
|
||||||
const generateHtmlContent = useCallback(() => {
|
const generateHtmlContent = useCallback(() => {
|
||||||
@@ -674,18 +667,14 @@ const PreviewFrame = ({
|
|||||||
const justCommitted = window.justCommittedInspectorChanges || false;
|
const justCommitted = window.justCommittedInspectorChanges || false;
|
||||||
|
|
||||||
if (isInspectorActive && !isGenuineCodeChange) {
|
if (isInspectorActive && !isGenuineCodeChange) {
|
||||||
console.log('🚫 SKIP REFRESH: Inspector is active, iframe DOM is source of truth');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (justCommitted) {
|
if (justCommitted) {
|
||||||
console.log('🚫 SKIP REFRESH: Just committed inspector changes, preventing immediate refresh');
|
|
||||||
window.justCommittedInspectorChanges = false;
|
window.justCommittedInspectorChanges = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ IFRAME UPDATE: Proceeding with iframe update (genuine code change or inspector inactive)');
|
|
||||||
|
|
||||||
// Update clean content refs
|
// Update clean content refs
|
||||||
lastCleanHtmlRef.current = cleanHtml;
|
lastCleanHtmlRef.current = cleanHtml;
|
||||||
lastCleanCssRef.current = cleanCss;
|
lastCleanCssRef.current = cleanCss;
|
||||||
@@ -717,7 +706,6 @@ const PreviewFrame = ({
|
|||||||
|
|
||||||
// Skip update only if normalized content hasn't changed AND no device change
|
// Skip update only if normalized content hasn't changed AND no device change
|
||||||
if (normalizedNew === normalizedLast && lastContentRef.current !== '' && !isDeviceChange) {
|
if (normalizedNew === normalizedLast && lastContentRef.current !== '' && !isDeviceChange) {
|
||||||
console.log('📋 Content unchanged and no device change, skipping update');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,7 +728,6 @@ const PreviewFrame = ({
|
|||||||
|
|
||||||
// Add safety check for document readiness
|
// Add safety check for document readiness
|
||||||
if (!doc || !doc.body) {
|
if (!doc || !doc.body) {
|
||||||
console.log('⚠️ Document not ready for content update, using fallback...');
|
|
||||||
// Fallback to document.write for safety
|
// Fallback to document.write for safety
|
||||||
if (iframe.contentDocument) {
|
if (iframe.contentDocument) {
|
||||||
iframe.contentDocument.open();
|
iframe.contentDocument.open();
|
||||||
@@ -773,13 +760,10 @@ const PreviewFrame = ({
|
|||||||
try {
|
try {
|
||||||
// SMART SCROLL RESTORATION: Only restore for genuine code changes, not inspector operations
|
// SMART SCROLL RESTORATION: Only restore for genuine code changes, not inspector operations
|
||||||
if (skipScrollRestorationRef.current) {
|
if (skipScrollRestorationRef.current) {
|
||||||
console.log('🚫 SKIP: Scroll restoration skipped for inspector operation');
|
|
||||||
skipScrollRestorationRef.current = false;
|
skipScrollRestorationRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔄 SMART RESTORE: Attempting scroll restoration for genuine code change');
|
|
||||||
|
|
||||||
const storedScroll = window.localStorage.getItem('htmlPreview_scrollPosition');
|
const storedScroll = window.localStorage.getItem('htmlPreview_scrollPosition');
|
||||||
|
|
||||||
if (!storedScroll) {
|
if (!storedScroll) {
|
||||||
@@ -842,7 +826,6 @@ const PreviewFrame = ({
|
|||||||
|
|
||||||
if (hasInspectorEdits && !isDeviceChange) {
|
if (hasInspectorEdits && !isDeviceChange) {
|
||||||
// INSPECTOR FIELD EDIT - Visual-only iframe update (no scroll disruption)
|
// INSPECTOR FIELD EDIT - Visual-only iframe update (no scroll disruption)
|
||||||
console.log('📝 INSPECTOR SAVE: Updating iframe content');
|
|
||||||
// Skip scroll restoration for inspector edits
|
// Skip scroll restoration for inspector edits
|
||||||
skipScrollRestorationRef.current = true;
|
skipScrollRestorationRef.current = true;
|
||||||
|
|
||||||
@@ -865,20 +848,16 @@ const PreviewFrame = ({
|
|||||||
editedElement.setAttribute(attr.name, attr.value);
|
editedElement.setAttribute(attr.name, attr.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🚀 Visual-only update complete - no scroll disruption');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// CODE/DEVICE CHANGE - Full content reload with scroll preservation
|
// CODE/DEVICE CHANGE - Full content reload with scroll preservation
|
||||||
console.log('🔄 Code or device change: Full content reload');
|
|
||||||
|
|
||||||
const currentBody = doc.body;
|
const currentBody = doc.body;
|
||||||
const tempDiv = doc.createElement('div');
|
const tempDiv = doc.createElement('div');
|
||||||
tempDiv.innerHTML = newContent.match(/<body[^>]*>([\s\S]*)<\/body>/i)?.[1] || newContent;
|
tempDiv.innerHTML = newContent.match(/<body[^>]*>([\s\S]*)<\/body>/i)?.[1] || newContent;
|
||||||
|
|
||||||
if (isDeviceChange) {
|
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
|
// Cancel any active inspector session by generating clean content from code boxes
|
||||||
const cleanContent = generateHtmlContent();
|
const cleanContent = generateHtmlContent();
|
||||||
@@ -892,15 +871,12 @@ const PreviewFrame = ({
|
|||||||
const restoreScrollAfterDeviceChange = () => {
|
const restoreScrollAfterDeviceChange = () => {
|
||||||
try {
|
try {
|
||||||
const storedScroll = localStorage.getItem('htmlPreview_scrollPosition');
|
const storedScroll = localStorage.getItem('htmlPreview_scrollPosition');
|
||||||
console.log('📍 Device change: Attempting scroll restoration...', { storedScroll });
|
|
||||||
|
|
||||||
if (!storedScroll) {
|
if (!storedScroll) {
|
||||||
console.log('⚠️ No stored scroll position found');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!iframe.contentWindow) {
|
if (!iframe.contentWindow) {
|
||||||
console.log('⚠️ iframe.contentWindow is null, retrying in 100ms...');
|
|
||||||
setTimeout(restoreScrollAfterDeviceChange, 100);
|
setTimeout(restoreScrollAfterDeviceChange, 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -909,7 +885,6 @@ const PreviewFrame = ({
|
|||||||
if (parsed && typeof parsed.x === 'number' && typeof parsed.y === 'number') {
|
if (parsed && typeof parsed.x === 'number' && typeof parsed.y === 'number') {
|
||||||
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
|
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
|
||||||
if (!doc || !doc.body || !iframe.contentWindow) {
|
if (!doc || !doc.body || !iframe.contentWindow) {
|
||||||
console.log('⚠️ Device change: Document or contentWindow not ready, retrying...');
|
|
||||||
setTimeout(restoreScrollAfterDeviceChange, 100);
|
setTimeout(restoreScrollAfterDeviceChange, 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -920,21 +895,10 @@ const PreviewFrame = ({
|
|||||||
const safeX = Math.min(parsed.x, maxScrollX);
|
const safeX = Math.min(parsed.x, maxScrollX);
|
||||||
const safeY = Math.min(parsed.y, maxScrollY);
|
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);
|
iframe.contentWindow.scrollTo(safeX, safeY);
|
||||||
console.log(`✅ Device change: Scroll restored to ${safeX}, ${safeY}`);
|
|
||||||
} else {
|
|
||||||
console.log('⚠️ Invalid scroll data:', parsed);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ Device change scroll restoration error:', e);
|
// Silently handle error
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -942,10 +906,7 @@ const PreviewFrame = ({
|
|||||||
setTimeout(restoreScrollAfterDeviceChange, 100);
|
setTimeout(restoreScrollAfterDeviceChange, 100);
|
||||||
setTimeout(restoreScrollAfterDeviceChange, 300);
|
setTimeout(restoreScrollAfterDeviceChange, 300);
|
||||||
setTimeout(restoreScrollAfterDeviceChange, 500);
|
setTimeout(restoreScrollAfterDeviceChange, 500);
|
||||||
|
|
||||||
console.log('📱 Device change: Fresh reload with clean content completed');
|
|
||||||
} else {
|
} else {
|
||||||
console.log('📝 Code change detected - reload with scroll preservation');
|
|
||||||
|
|
||||||
currentBody.innerHTML = tempDiv.innerHTML;
|
currentBody.innerHTML = tempDiv.innerHTML;
|
||||||
|
|
||||||
@@ -953,15 +914,12 @@ const PreviewFrame = ({
|
|||||||
const restoreScrollToElement = () => {
|
const restoreScrollToElement = () => {
|
||||||
try {
|
try {
|
||||||
const storedScroll = localStorage.getItem('htmlPreview_scrollPosition');
|
const storedScroll = localStorage.getItem('htmlPreview_scrollPosition');
|
||||||
console.log('📍 Code change: Attempting element-based scroll restoration...', { storedScroll });
|
|
||||||
|
|
||||||
if (!storedScroll) {
|
if (!storedScroll) {
|
||||||
console.log('⚠️ Code change: No stored scroll data found');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!iframe.contentWindow) {
|
if (!iframe.contentWindow) {
|
||||||
console.log('⚠️ Code change: iframe.contentWindow not ready, retrying...');
|
|
||||||
setTimeout(restoreScrollToElement, 50);
|
setTimeout(restoreScrollToElement, 50);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -970,7 +928,6 @@ const PreviewFrame = ({
|
|||||||
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
|
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
|
||||||
|
|
||||||
if (!doc || !doc.body || !iframe.contentWindow) {
|
if (!doc || !doc.body || !iframe.contentWindow) {
|
||||||
console.log('⚠️ Code change: Document or contentWindow not ready, retrying...');
|
|
||||||
setTimeout(restoreScrollToElement, 50);
|
setTimeout(restoreScrollToElement, 50);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -980,10 +937,8 @@ const PreviewFrame = ({
|
|||||||
const targetElement = doc.querySelector(parsed.elementSelector);
|
const targetElement = doc.querySelector(parsed.elementSelector);
|
||||||
if (targetElement) {
|
if (targetElement) {
|
||||||
targetElement.scrollIntoView({ behavior: 'instant', block: 'start' });
|
targetElement.scrollIntoView({ behavior: 'instant', block: 'start' });
|
||||||
console.log(`✅ Code change: Scrolled to element ${parsed.elementSelector}`);
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
console.log(`⚠️ Code change: Element ${parsed.elementSelector} not found, trying text match...`);
|
|
||||||
|
|
||||||
// Fallback: find element by text content
|
// Fallback: find element by text content
|
||||||
if (parsed.elementText) {
|
if (parsed.elementText) {
|
||||||
@@ -991,7 +946,6 @@ const PreviewFrame = ({
|
|||||||
for (const el of allElements) {
|
for (const el of allElements) {
|
||||||
if (el.textContent && el.textContent.includes(parsed.elementText.substring(0, 20))) {
|
if (el.textContent && el.textContent.includes(parsed.elementText.substring(0, 20))) {
|
||||||
el.scrollIntoView({ behavior: 'instant', block: 'start' });
|
el.scrollIntoView({ behavior: 'instant', block: 'start' });
|
||||||
console.log(`✅ Code change: Scrolled to element by text match`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1009,12 +963,11 @@ const PreviewFrame = ({
|
|||||||
|
|
||||||
if (safeX > 0 || safeY > 0) {
|
if (safeX > 0 || safeY > 0) {
|
||||||
iframe.contentWindow.scrollTo(safeX, safeY);
|
iframe.contentWindow.scrollTo(safeX, safeY);
|
||||||
console.log(`✅ Code change: Fallback coordinate scroll to ${safeX}, ${safeY}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} 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, 50);
|
||||||
setTimeout(restoreScrollToElement, 150);
|
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 => {
|
// Update head styles
|
||||||
const styleEl = doc.createElement('style');
|
const styleMatch = newContent.match(/<style[^>]*>([\s\S]*?)<\/style>/gi);
|
||||||
styleEl.textContent = styleTag.replace(/<\/?style[^>]*>/gi, '');
|
if (styleMatch) {
|
||||||
doc.head.appendChild(styleEl);
|
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) {
|
} catch (error) {
|
||||||
console.error('Error updating iframe content:', error);
|
|
||||||
// Fallback to document.write if selective update fails
|
// Fallback to document.write if selective update fails
|
||||||
try {
|
try {
|
||||||
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
|
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
|
||||||
doc.open();
|
doc.open();
|
||||||
doc.write(newContent);
|
doc.write(newContent);
|
||||||
doc.close();
|
doc.close();
|
||||||
console.log('🔄 Fallback to full rewrite');
|
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.error('Fallback update also failed:', fallbackError);
|
// Fallback update failed
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoadingRef.current = false;
|
isLoadingRef.current = false;
|
||||||
@@ -1074,10 +1024,8 @@ const PreviewFrame = ({
|
|||||||
if (!doc || !doc.body) return;
|
if (!doc || !doc.body) return;
|
||||||
|
|
||||||
if (inspectMode) {
|
if (inspectMode) {
|
||||||
console.log('🎨 Applying inspect mode styles.');
|
|
||||||
setupInspectModeStyles(doc);
|
setupInspectModeStyles(doc);
|
||||||
} else {
|
} else {
|
||||||
console.log('🧹 Removing inspect mode styles.');
|
|
||||||
// Remove inspect mode styles when disabled
|
// Remove inspect mode styles when disabled
|
||||||
const existingStyle = doc.getElementById('inspect-mode-styles');
|
const existingStyle = doc.getElementById('inspect-mode-styles');
|
||||||
if (existingStyle) {
|
if (existingStyle) {
|
||||||
@@ -1104,7 +1052,6 @@ const PreviewFrame = ({
|
|||||||
const currentX = win.pageXOffset || win.scrollX || 0;
|
const currentX = win.pageXOffset || win.scrollX || 0;
|
||||||
const currentY = win.pageYOffset || win.scrollY || 0;
|
const currentY = win.pageYOffset || win.scrollY || 0;
|
||||||
|
|
||||||
console.log(`📊 TRACKING SCROLL: Current position (${currentX}, ${currentY})`);
|
|
||||||
scrollPositionRef.current = { x: currentX, y: currentY };
|
scrollPositionRef.current = { x: currentX, y: currentY };
|
||||||
|
|
||||||
// Store scroll position with element-based tracking
|
// Store scroll position with element-based tracking
|
||||||
@@ -1113,7 +1060,6 @@ const PreviewFrame = ({
|
|||||||
try {
|
try {
|
||||||
// Store in PARENT window localStorage, not iframe localStorage
|
// Store in PARENT window localStorage, not iframe localStorage
|
||||||
window.localStorage.setItem('htmlPreview_scrollPosition', JSON.stringify(scrollData));
|
window.localStorage.setItem('htmlPreview_scrollPosition', JSON.stringify(scrollData));
|
||||||
console.log(`💾 STORED SCROLL: Saved position (${currentX}, ${currentY}) to localStorage`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silently handle localStorage errors
|
// Silently handle localStorage errors
|
||||||
}
|
}
|
||||||
@@ -1191,11 +1137,9 @@ const PreviewFrame = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getDeviceWrapper = () => {
|
const getDeviceWrapper = () => {
|
||||||
console.log('🔧 Device Frame Debug:', { isFullscreen, selectedDevice });
|
|
||||||
|
|
||||||
// Non-fullscreen always uses iPhone frame (mobile view)
|
// Non-fullscreen always uses iPhone frame (mobile view)
|
||||||
if (!isFullscreen) {
|
if (!isFullscreen) {
|
||||||
console.log('📱 Non-fullscreen: Using iPhone 14 Pro frame');
|
|
||||||
return {
|
return {
|
||||||
wrapperClass: 'flex justify-center items-center w-full h-full',
|
wrapperClass: 'flex justify-center items-center w-full h-full',
|
||||||
deviceFrame: 'iphone-14-pro'
|
deviceFrame: 'iphone-14-pro'
|
||||||
@@ -1204,7 +1148,6 @@ const PreviewFrame = ({
|
|||||||
|
|
||||||
// Fullscreen desktop mode: no frame
|
// Fullscreen desktop mode: no frame
|
||||||
if (selectedDevice === 'desktop') {
|
if (selectedDevice === 'desktop') {
|
||||||
console.log('🖥️ Desktop fullscreen: No device frame');
|
|
||||||
return {
|
return {
|
||||||
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
|
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
|
||||||
deviceFrame: null
|
deviceFrame: null
|
||||||
@@ -1213,19 +1156,16 @@ const PreviewFrame = ({
|
|||||||
|
|
||||||
switch (selectedDevice) {
|
switch (selectedDevice) {
|
||||||
case 'tablet':
|
case 'tablet':
|
||||||
console.log('📟 Rendering iPad Pro frame');
|
|
||||||
return {
|
return {
|
||||||
wrapperClass: 'flex justify-center items-center w-full h-full',
|
wrapperClass: 'flex justify-center items-center w-full h-full',
|
||||||
deviceFrame: 'ipad-pro'
|
deviceFrame: 'ipad-pro'
|
||||||
};
|
};
|
||||||
case 'mobile':
|
case 'mobile':
|
||||||
console.log('📱 Rendering iPhone 14 Pro frame');
|
|
||||||
return {
|
return {
|
||||||
wrapperClass: 'flex justify-center items-center w-full h-full',
|
wrapperClass: 'flex justify-center items-center w-full h-full',
|
||||||
deviceFrame: 'iphone-14-pro'
|
deviceFrame: 'iphone-14-pro'
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
console.log('❓ Unknown device, no frame');
|
|
||||||
return {
|
return {
|
||||||
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
|
wrapperClass: 'w-full h-full max-w-full overflow-hidden',
|
||||||
deviceFrame: null
|
deviceFrame: null
|
||||||
@@ -1242,7 +1182,6 @@ const PreviewFrame = ({
|
|||||||
|
|
||||||
if (deviceFrame) {
|
if (deviceFrame) {
|
||||||
// Render with device frame (iPhone 14 Pro or iPad Pro)
|
// Render with device frame (iPhone 14 Pro or iPad Pro)
|
||||||
console.log(`🎨 Rendering device frame: device-${deviceFrame}`);
|
|
||||||
return (
|
return (
|
||||||
<div className={wrapperClass}>
|
<div className={wrapperClass}>
|
||||||
<div className={`device device-${deviceFrame}`}>
|
<div className={`device device-${deviceFrame}`}>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ const Toolbar = ({
|
|||||||
cleanupInspectorState();
|
cleanupInspectorState();
|
||||||
} else {
|
} else {
|
||||||
// Activate inspect mode (no need to cleanup when activating)
|
// Activate inspect mode (no need to cleanup when activating)
|
||||||
console.log('🎯 TOOLBAR: Activating inspect mode');
|
|
||||||
setInspectMode(true);
|
setInspectMode(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// Google Analytics utility for React SPA
|
// Google Analytics utility for React SPA
|
||||||
// Implements best practices for Single Page Applications
|
// Implements best practices for Single Page Applications
|
||||||
|
|
||||||
|
import { initConsentMode, applyStoredConsent } from './consentManager';
|
||||||
|
|
||||||
// Google Analytics configuration
|
// 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
|
// Initialize Google Analytics with Consent Mode v2
|
||||||
export const initGA = () => {
|
export const initGA = () => {
|
||||||
@@ -22,7 +24,6 @@ export const initGA = () => {
|
|||||||
window.gtag = gtag;
|
window.gtag = gtag;
|
||||||
|
|
||||||
// Initialize Consent Mode v2 BEFORE loading GA script
|
// Initialize Consent Mode v2 BEFORE loading GA script
|
||||||
const { initConsentMode, applyStoredConsent } = require('./consentManager');
|
|
||||||
initConsentMode();
|
initConsentMode();
|
||||||
|
|
||||||
// Create script elements
|
// Create script elements
|
||||||
|
|||||||
@@ -122,18 +122,14 @@ export const initBrowserCompat = () => {
|
|||||||
|
|
||||||
// Add specific fixes for Telegram browser
|
// Add specific fixes for Telegram browser
|
||||||
if (browserInfo.isTelegram) {
|
if (browserInfo.isTelegram) {
|
||||||
console.log('Telegram browser detected - applying compatibility fixes');
|
|
||||||
|
|
||||||
// Add Telegram-specific error handling
|
// Add Telegram-specific error handling
|
||||||
window.addEventListener('error', (event) => {
|
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
|
// Prevent the error from bubbling up and showing the error overlay
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('unhandledrejection', (event) => {
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
console.log('Unhandled promise rejection in Telegram browser:', event.reason);
|
|
||||||
// Prevent the error from bubbling up
|
// Prevent the error from bubbling up
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export const getConsentBannerData = () => {
|
|||||||
{
|
{
|
||||||
id: CONSENT_CATEGORIES.ADVERTISING,
|
id: CONSENT_CATEGORIES.ADVERTISING,
|
||||||
name: 'Advertising',
|
name: 'Advertising',
|
||||||
description: 'Future ad personalization (not yet implemented)',
|
description: 'Ad personalization and targeting (Adsterra)',
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const fetchUrlContent = async (url) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (directError) {
|
} catch (directError) {
|
||||||
console.log('Direct fetch failed, trying CORS proxy:', directError.message);
|
// Direct fetch failed, trying CORS proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try CORS proxies
|
// Try CORS proxies
|
||||||
@@ -124,7 +124,6 @@ export const fetchUrlContent = async (url) => {
|
|||||||
}
|
}
|
||||||
} catch (proxyError) {
|
} catch (proxyError) {
|
||||||
lastError = proxyError;
|
lastError = proxyError;
|
||||||
console.log(`Proxy ${proxy} failed:`, proxyError.message);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -44,7 +44,6 @@ export const fetchGitHubReleases = async (owner, repo, token = null) => {
|
|||||||
url: commit.html_url
|
url: commit.html_url
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch GitHub releases:', error);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -79,7 +78,6 @@ export const fetchGiteaReleases = async (owner, repo, token, baseUrl) => {
|
|||||||
url: `${baseUrl}/${owner}/${repo}/commit/${commit.sha}`
|
url: `${baseUrl}/${owner}/${repo}/commit/${commit.sha}`
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch Gitea releases:', error);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -95,7 +93,6 @@ export const fetchCustomReleases = async (apiEndpoint) => {
|
|||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch custom releases:', error);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -111,7 +108,6 @@ export const fetchStaticReleases = async () => {
|
|||||||
|
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch static releases:', error);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export const getCoreWebVitalsOptimizations = () => {
|
|||||||
// Cumulative Layout Shift (CLS)
|
// Cumulative Layout Shift (CLS)
|
||||||
cls: {
|
cls: {
|
||||||
setImageDimensions: true,
|
setImageDimensions: true,
|
||||||
reserveSpaceForAds: true, // Important for future AdSense
|
reserveSpaceForAds: true,
|
||||||
avoidDynamicContent: true,
|
avoidDynamicContent: true,
|
||||||
useTransforms: true
|
useTransforms: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
Reference in New Issue
Block a user