From 9dc3285adb59e95376c02cf8d5287191a48519c8 Mon Sep 17 00:00:00 2001 From: dwindown Date: Wed, 18 Feb 2026 16:50:45 +0700 Subject: [PATCH] feat: Adsterra integration, code splitting, cleanup, Onidel affiliate --- .env | 1 + .env.example | 2 + ADSENSE_REVISED_STRATEGY.md | 436 ----------- ADSENSE_SETUP_GUIDE.md | 252 ------ ADSENSE_STRATEGY.md | 446 ----------- README.md | 201 ++--- SEO_IMPROVEMENT_PLAN.md | 32 +- package-lock.json | 1 - package.json | 5 +- public/images/onidel-banner.webp | Bin 0 -> 28674 bytes public/index.html | 4 +- public/manifest.json | 12 + public/sitemap.xml | 6 - src/App.js | 42 +- src/components/AdBlock.js | 67 +- src/components/AdColumn.js | 28 +- src/components/AffiliateBlock.js | 21 + src/components/Loading.js | 18 + src/components/MobileAdBanner.js | 72 +- src/components/OfferBlock.js | 22 + src/pages/InvoiceEditor.js | 8 +- src/pages/InvoicePreview.js | 4 +- src/pages/InvoicePreviewMinimal.js | 2 - src/pages/MarkdownEditor.js | 12 +- src/pages/ObjectEditor.js | 13 +- src/pages/PrivacyPolicy.js | 13 +- src/pages/TermsOfService.js | 2 +- src/pages/components/ElementEditor.js | 27 +- src/pages/components/PreviewFrame.backup.js | 674 ---------------- .../components/PreviewFrame.experimental.js | 720 ------------------ src/pages/components/PreviewFrame.js | 93 +-- src/pages/components/Toolbar.js | 1 - src/utils/analytics.js | 5 +- src/utils/browserCompat.js | 4 - src/utils/consentManager.js | 2 +- src/utils/contentExtractor.js | 3 +- src/utils/currencies.json | 1 - src/utils/releaseNotesAPI.js | 4 - src/utils/seo.js | 2 +- temp_export_tabs.js | 27 - 40 files changed, 352 insertions(+), 2933 deletions(-) create mode 100644 .env create mode 100644 .env.example delete mode 100644 ADSENSE_REVISED_STRATEGY.md delete mode 100644 ADSENSE_SETUP_GUIDE.md delete mode 100644 ADSENSE_STRATEGY.md create mode 100644 public/images/onidel-banner.webp create mode 100644 src/components/AffiliateBlock.js create mode 100644 src/components/Loading.js create mode 100644 src/components/OfferBlock.js delete mode 100644 src/pages/components/PreviewFrame.backup.js delete mode 100644 src/pages/components/PreviewFrame.experimental.js delete mode 100644 src/utils/currencies.json delete mode 100644 temp_export_tabs.js diff --git a/.env b/.env new file mode 100644 index 00000000..aaa0a848 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +REACT_APP_GA_ID=G-S3K5P2PWV6 diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..ad6c88b2 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# Google Analytics Measurement ID +REACT_APP_GA_ID=G-S3K5P2PWV6 diff --git a/ADSENSE_REVISED_STRATEGY.md b/ADSENSE_REVISED_STRATEGY.md deleted file mode 100644 index bc0fb754..00000000 --- a/ADSENSE_REVISED_STRATEGY.md +++ /dev/null @@ -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 ( - - ); -}; - -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 ( -
- -
- ); -}; - -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 ( -
- -
- -
-
- ); -}; - -export default MobileAdBanner; -``` - -**Update ToolLayout:** - -```javascript -// src/components/ToolLayout.jsx -import AdColumn from './AdColumn'; -import MobileAdBanner from './MobileAdBanner'; - -const ToolLayout = ({ children }) => { - return ( -
- {/* Main Content */} -
- {children} -
- - {/* Desktop Ad Column */} - - - {/* Mobile Ad Banner */} - -
- ); -}; -``` - -### Phase 2: AdSense Integration (1 day) - -1. **Apply for AdSense** (if not done) -2. **Add script to index.html**: -```html - -``` -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. \ No newline at end of file diff --git a/ADSENSE_SETUP_GUIDE.md b/ADSENSE_SETUP_GUIDE.md deleted file mode 100644 index af32ca01..00000000 --- a/ADSENSE_SETUP_GUIDE.md +++ /dev/null @@ -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 `
` 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!** 🎉 diff --git a/ADSENSE_STRATEGY.md b/ADSENSE_STRATEGY.md deleted file mode 100644 index 05f4e0e0..00000000 --- a/ADSENSE_STRATEGY.md +++ /dev/null @@ -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 ( -
- -
- ); -}; - -export default AdSense; -``` - -### **Usage Example** - -```javascript -// In TableEditor.js -import AdSense from '../components/AdSense'; - -// Between sections - - -// Sidebar - -``` - -### **Add Script to index.html** - -```html - - - - - -``` - ---- - -## 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. diff --git a/README.md b/README.md index 5d7f15d1..6e3fa680 100644 --- a/README.md +++ b/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 ### Available Tools -1. **JSON Encoder/Decoder** - Format, validate, and minify JSON data with syntax highlighting -2. **Serialize Encoder/Decoder** - Encode and decode PHP serialized data -3. **URL Encoder/Decoder** - Encode and decode URLs and query parameters -4. **Base64 Encoder/Decoder** - Convert text to Base64 and back with file support -5. **CSV ↔ JSON Converter** - Convert between CSV and JSON formats with custom delimiters -6. **Code Beautifier/Minifier** - Format and minify JSON, XML, SQL, CSS, and HTML code -7. **Text Diff Checker** - Compare two texts and highlight differences line by line +1. **Object Editor** (`/object-editor`) — Visual editor for JSON and PHP serialized objects with mindmap visualization. +2. **Table Editor** (`/table-editor`) — Import, edit, and export tabular data from URLs, files, or paste CSV/JSON. +3. **Markdown Editor** (`/markdown-editor`) — Write and preview markdown with live rendering, syntax highlighting, and export. +4. **Invoice Editor** (`/invoice-editor`) — Create, edit, and export professional invoices with PDF generation. +5. **URL Encoder/Decoder** (`/url`) — Encode and decode URLs and query parameters. +6. **Base64 Encoder/Decoder** (`/base64`) — Convert text to Base64 and back with file support. +7. **Code Beautifier/Minifier** (`/beautifier`) — Format and minify JSON, XML, SQL, CSS, and HTML code. +8. **Text Diff Checker** (`/diff`) — Compare two texts and highlight differences line by line. +9. **Text Length Checker** (`/text-length`) — Analyze text length, word count, and other text statistics. ### Key Features -- ✨ **Modern UI** - Clean, minimalist design with automatic dark/light mode -- 📱 **Fully Responsive** - Equal experience on desktop and mobile devices -- ⚡ **Lightning Fast** - All processing happens locally in the browser -- 🔒 **Privacy First** - No server dependencies, no data collection -- 📋 **Easy Copy-Paste** - One-click copy functionality for all outputs -- 📁 **File Support** - Upload files directly for processing -- 🔍 **Searchable** - Quick search through available tools +- ✨ **Modern UI** — Clean, minimalist design with automatic dark/light mode. +- 📱 **Fully Responsive** — Equal experience on desktop and mobile devices. +- ⚡ **Lightning Fast** — Code-splitting with React.lazy for optimal loading performance. +- 🔒 **Privacy First** — No server dependencies, no data collection. +- 📋 **Easy Copy-Paste** — One-click copy functionality for all outputs. +- 📁 **File Support** — Upload files directly for processing. +- 🔍 **Searchable** — Quick search through available tools. +- 🎯 **SEO Optimized** — Pre-rendered pages with React Snap, meta tags, and sitemap. ## 🛠 Tech Stack -- **Frontend**: React 18, React Router -- **Styling**: TailwindCSS with custom design system +- **Frontend**: React 18, React Router 6 +- **Styling**: TailwindCSS 3 +- **Editor**: CodeMirror 6 (@uiw/react-codemirror) +- **PDF Generation**: jsPDF, jsPDF-AutoTable +- **Markdown**: marked, DOMPurify, highlight.js - **Icons**: Lucide React -- **Build Tool**: Create React App -- **Libraries**: - - js-beautify (code formatting) +- **SEO**: react-helmet-async +- **Analytics**: Google Analytics 4 with Consent Mode v2 +- **Advertising**: Adsterra +- **Utilities**: - papaparse (CSV parsing) + - js-beautify (code formatting) - serialize-javascript (serialization) + - diff-match-patch (text comparison) + - turndown (HTML to Markdown) ## 📦 Installation 1. Clone the repository: -```bash -git clone -cd developer-tools -``` + ```bash + git clone + cd developer-tools + ``` 2. Install dependencies: -```bash -npm install -``` + ```bash + npm install + ``` -3. Start the development server: -```bash -npm start -``` +3. Configure environment (optional): + ```bash + cp .env.example .env + # Edit .env with your GA measurement ID + ``` -4. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +4. Start the development server: + ```bash + npm run dev + # or + npm start + ``` + +5. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. ## 🏗 Project Structure ``` src/ -├── components/ # Reusable UI components -│ ├── Layout.js # Main layout wrapper -│ ├── ToolCard.js # Tool card component for home page -│ ├── ToolLayout.js # Layout for individual tools -│ └── CopyButton.js # Copy to clipboard button -├── pages/ # Individual tool pages -│ ├── Home.js # Homepage with tool listing -│ ├── JsonTool.js # JSON encoder/decoder -│ ├── SerializeTool.js # Serialize encoder/decoder -│ ├── UrlTool.js # URL encoder/decoder -│ ├── Base64Tool.js # Base64 encoder/decoder -│ ├── CsvJsonTool.js # CSV/JSON converter -│ ├── BeautifierTool.js# Code beautifier/minifier -│ └── DiffTool.js # Text diff checker -├── App.js # Main app component with routing -├── index.js # React app entry point -└── index.css # Global styles and Tailwind imports +├── components/ # Reusable UI components +│ ├── Layout.js # Main layout with sidebar navigation +│ ├── ToolLayout.js # Wrapper for tool pages with ad slots +│ ├── ToolCard.js # Tool card for homepage +│ ├── ToolSidebar.js # Sidebar navigation +│ ├── AdBlock.js # Adsterra ad unit wrapper +│ ├── AdColumn.js # Desktop sidebar ad column +│ ├── MobileAdBanner.js # Mobile bottom ad banner +│ ├── OfferBlock.js # Promotional offer slot +│ ├── AffiliateBlock.js # Affiliate link slot +│ ├── Loading.js # Loading spinner for Suspense +│ ├── CodeMirrorEditor.js # Code editor component +│ ├── CodeEditor.js # Alternative code editor +│ ├── SEO.js / SEOHead.js # SEO meta tags +│ ├── RelatedTools.js # Related tools suggestions +│ ├── ErrorBoundary.js # Error boundary +│ ├── ThemeToggle.js # Dark/light mode toggle +│ ├── CopyButton.js # One-click copy +│ ├── ConsentBanner.js # GDPR consent banner +│ ├── MindmapView.js # JSON visualization +│ ├── StructuredEditor.js # Object editor modal +│ └── ProBadge.js # PRO feature badge +├── pages/ # Tool pages (lazy-loaded) +│ ├── Home.js +│ ├── ObjectEditor.js +│ ├── TableEditor.js +│ ├── MarkdownEditor.js +│ ├── InvoiceEditor.js +│ ├── InvoicePreview.js +│ ├── InvoicePreviewMinimal.js +│ ├── BeautifierTool.js +│ ├── UrlTool.js +│ ├── Base64Tool.js +│ ├── DiffTool.js +│ ├── TextLengthTool.js +│ ├── ReleaseNotes.js +│ ├── PrivacyPolicy.js +│ ├── TermsOfService.js +│ └── NotFound.js +├── config/ # App configuration +│ ├── tools.js # Tool definitions (single source of truth) +│ └── features.js # Feature flags (FREE/PRO tiers) +├── utils/ # Utility functions +│ ├── analytics.js # Google Analytics 4 +│ ├── consentManager.js # GDPR consent management +│ ├── seo.js # SEO helpers +│ ├── browserCompat.js # Browser compatibility +│ ├── contentExtractor.js # Content parsing +│ └── sitemapGenerator.js # Sitemap utilities +└── hooks/ # Custom React hooks + ├── useAnalytics.js # Analytics hook + └── useNavigationGuard.js # Unsaved changes protection ``` -## 🎨 Design System - -The application uses a consistent design system built with TailwindCSS: - -- **Colors**: Primary blue theme with automatic dark/light mode -- **Typography**: System fonts with JetBrains Mono for code -- **Components**: Reusable utility classes for consistent styling -- **Responsive**: Mobile-first design approach - ## 🚀 Available Scripts -- `npm start` - Runs the app in development mode -- `npm build` - Builds the app for production -- `npm test` - Launches the test runner -- `npm run eject` - Ejects from Create React App (one-way operation) +- `npm run dev` or `npm start` — Runs the app in development mode. +- `npm run build` — Builds the app for production (with React Snap for pre-rendering). +- `npm run build:no-snap` — Builds the app for production (without pre-rendering). +- `npm test` — Launches the test runner. +- `npm run eject` — Ejects from Create React App (one-way operation). -## 📱 Usage +## 📁 Environment Variables -1. **Home Page**: Browse and search through available tools -2. **Individual Tools**: Each tool has a dedicated page with: - - Input area for your data - - Processing controls (encode/decode, beautify/minify, etc.) - - Output area with copy functionality - - File upload support where applicable - - Usage tips and examples +| Variable | Description | Default | +|----------|-------------|---------| +| `REACT_APP_GA_ID` | Google Analytics Measurement ID | `G-S3K5P2PWV6` | -## 🔧 Customization - -The application is built with scalability in mind: - -- **Adding New Tools**: Create a new page component and add it to the routing -- **Styling**: Modify `tailwind.config.js` for theme customization -- **Components**: All components are modular and reusable +Create a `.env` file based on `.env.example` to override defaults. ## 🌟 Contributing @@ -123,13 +158,3 @@ The application is built with scalability in mind: ## 📄 License This project is open source and available under the MIT License. - -## 🎯 Roadmap - -Future enhancements could include: -- More encoding/decoding formats -- Advanced diff algorithms -- Syntax highlighting for code -- Export functionality -- Keyboard shortcuts -- Tool favorites/bookmarks diff --git a/SEO_IMPROVEMENT_PLAN.md b/SEO_IMPROVEMENT_PLAN.md index 7ddbe5c3..1c2b4719 100644 --- a/SEO_IMPROVEMENT_PLAN.md +++ b/SEO_IMPROVEMENT_PLAN.md @@ -19,12 +19,10 @@ ### 1. **Add Missing Tools to Sitemap** -**Current Issue:** Sitemap is missing several tools: -- ❌ Markdown Editor -- ❌ JSON Tool -- ❌ CSV/JSON Converter -- ❌ Serialize Tool -- ❌ Release Notes page +**Current Issue:** Sitemap was missing the Markdown Editor and Release Notes pages. + +> **Note:** `/json`, `/csv-json`, and `/serialize` routes were planned but never implemented. +> These tools are now consolidated into the Object Editor (`/object-editor`). **Action:** Update `public/sitemap.xml` @@ -36,24 +34,6 @@ monthly 0.9 - - https://dewe.dev/json - 2025-10-22 - monthly - 0.8 - - - https://dewe.dev/csv-json - 2025-10-22 - monthly - 0.8 - - - https://dewe.dev/serialize - 2025-10-22 - monthly - 0.8 - https://dewe.dev/release-notes 2025-10-22 @@ -296,8 +276,8 @@ Write and preview markdown with live rendering **Add to `index.html`:** ```html - - + + ``` --- diff --git a/package-lock.json b/package-lock.json index 6c3bfea4..01050357 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,6 @@ "react-dom": "18.3.1", "react-helmet-async": "^2.0.5", "react-router-dom": "6.26.2", - "react-scripts": "5.0.1", "react-snap": "^1.23.0", "reactflow": "^11.11.4", "serialize-javascript": "^6.0.0", diff --git a/package.json b/package.json index d3a9d295..e3a93f1f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "description": "Web Developer Tools MVP - Utilities Toolkit", "private": true, "dependencies": { - "@codemirror/basic-setup": "^0.20.0", "@codemirror/commands": "^6.8.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.9", @@ -41,7 +40,6 @@ "react-dom": "18.3.1", "react-helmet-async": "^2.0.5", "react-router-dom": "6.26.2", - "react-scripts": "5.0.1", "react-snap": "^1.23.0", "reactflow": "^11.11.4", "serialize-javascript": "^6.0.0", @@ -56,6 +54,7 @@ "tailwindcss": "^3.3.0" }, "scripts": { + "dev": "react-scripts start", "start": "react-scripts start", "start:prod": "serve -s build -l 3000", "build": "react-scripts build && react-snap", @@ -83,7 +82,7 @@ "/beautifier", "/diff", "/text-length", - "/whats-new", + "/markdown-editor", "/privacy", "/terms" ] diff --git a/public/images/onidel-banner.webp b/public/images/onidel-banner.webp new file mode 100644 index 0000000000000000000000000000000000000000..b12854294f058043eb65d22956a84d584c900d85 GIT binary patch literal 28674 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 M6ob{$IpQDy0P0cmIsgCw literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html index df99a2a6..f003055e 100644 --- a/public/index.html +++ b/public/index.html @@ -14,9 +14,7 @@ Developer Tools - Web Developer Utilities - - + diff --git a/public/manifest.json b/public/manifest.json index a6adb688..5734231e 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -6,6 +6,18 @@ "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" + }, + { + "src": "icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" } ], "start_url": ".", diff --git a/public/sitemap.xml b/public/sitemap.xml index ba56e80e..7247c47d 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -72,12 +72,6 @@ - - https://dewe.dev/whats-new - 2025-10-22 - weekly - 0.7 - https://dewe.dev/release-notes 2025-10-22 diff --git a/src/App.js b/src/App.js index 100514ea..72e321e6 100644 --- a/src/App.js +++ b/src/App.js @@ -1,28 +1,30 @@ -import React, { useEffect } from 'react'; -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import React, { useEffect, Suspense, lazy } from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { HelmetProvider } from 'react-helmet-async'; import Layout from './components/Layout'; import ErrorBoundary from './components/ErrorBoundary'; -import Home from './pages/Home'; -import UrlTool from './pages/UrlTool'; -import Base64Tool from './pages/Base64Tool'; -import BeautifierTool from './pages/BeautifierTool'; -import DiffTool from './pages/DiffTool'; -import TextLengthTool from './pages/TextLengthTool'; -import ObjectEditor from './pages/ObjectEditor'; -import TableEditor from './pages/TableEditor'; -import InvoiceEditor from './pages/InvoiceEditor'; -import MarkdownEditor from './pages/MarkdownEditor'; -import InvoicePreview from './pages/InvoicePreview'; -import InvoicePreviewMinimal from './pages/InvoicePreviewMinimal'; -import ReleaseNotes from './pages/ReleaseNotes'; -import TermsOfService from './pages/TermsOfService'; -import PrivacyPolicy from './pages/PrivacyPolicy'; -import NotFound from './pages/NotFound'; +import Loading from './components/Loading'; import { initGA } from './utils/analytics'; import './index.css'; +const Home = lazy(() => import('./pages/Home')); +const UrlTool = lazy(() => import('./pages/UrlTool')); +const Base64Tool = lazy(() => import('./pages/Base64Tool')); +const BeautifierTool = lazy(() => import('./pages/BeautifierTool')); +const DiffTool = lazy(() => import('./pages/DiffTool')); +const TextLengthTool = lazy(() => import('./pages/TextLengthTool')); +const ObjectEditor = lazy(() => import('./pages/ObjectEditor')); +const TableEditor = lazy(() => import('./pages/TableEditor')); +const InvoiceEditor = lazy(() => import('./pages/InvoiceEditor')); +const MarkdownEditor = lazy(() => import('./pages/MarkdownEditor')); +const InvoicePreview = lazy(() => import('./pages/InvoicePreview')); +const InvoicePreviewMinimal = lazy(() => import('./pages/InvoicePreviewMinimal')); +const ReleaseNotes = lazy(() => import('./pages/ReleaseNotes')); +const TermsOfService = lazy(() => import('./pages/TermsOfService')); +const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy')); +const NotFound = lazy(() => import('./pages/NotFound')); + function App() { // Initialize Google Analytics on app startup useEffect(() => { @@ -34,6 +36,7 @@ function App() { + }> } /> } /> @@ -47,12 +50,13 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> } /> + diff --git a/src/components/AdBlock.js b/src/components/AdBlock.js index e9748e6a..5ed96b52 100644 --- a/src/components/AdBlock.js +++ b/src/components/AdBlock.js @@ -1,35 +1,48 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; + +const AdBlock = ({ className = '', adKey = 'e0ca7c61c83457f093bbc2e261b43d31' }) => { + const iframeRef = useRef(null); -/** - * AdBlock Component - Individual ad unit - * Displays a single Google AdSense ad - */ -const AdBlock = ({ slot, size = '300x250', className = '' }) => { useEffect(() => { - try { - // Push ad to AdSense queue - (window.adsbygoogle = window.adsbygoogle || []).push({}); - } catch (e) { - console.error('AdSense error:', e); - } - }, []); + if (!iframeRef.current) return; - const [width, height] = size.split('x'); + const iframe = iframeRef.current; + const doc = iframe.contentDocument || iframe.contentWindow.document; + + doc.open(); + doc.write(` + + + + + + + + + + + `); + doc.close(); + }, [adKey]); return ( -
- -
+