feat: major update - Markdown Editor, CodeMirror upgrades, SEO improvements, tool cleanup

- Added new Markdown Editor with live preview, GFM support, PDF/HTML/DOCX export
- Upgraded all paste fields to CodeMirror with syntax highlighting and expand/collapse
- Enhanced Object Editor with advanced URL fetching and preview mode
- Improved export views with syntax highlighting in Table/Object editors
- Implemented SEO improvements (FAQ schema, breadcrumbs, internal linking)
- Added Related Tools recommendations component
- Created custom 404 page with tool suggestions
- Consolidated tools: removed JSON, Serialize, CSV-JSON (merged into main editors)
- Updated documentation and cleaned up redundant files
- Updated release notes with user-centric improvements
This commit is contained in:
dwindown
2025-10-22 15:20:22 +07:00
parent 08d345eaeb
commit fb9c944366
40 changed files with 6927 additions and 1714 deletions

436
ADSENSE_REVISED_STRATEGY.md Normal file
View File

@@ -0,0 +1,436 @@
# 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.

252
ADSENSE_SETUP_GUIDE.md Normal file
View File

@@ -0,0 +1,252 @@
# 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!** 🎉

446
ADSENSE_STRATEGY.md Normal file
View File

@@ -0,0 +1,446 @@
# 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.

331
BACKEND_REQUIREMENTS.md Normal file
View File

@@ -0,0 +1,331 @@
# Backend Requirements for Production
## Question 2: Does Advanced URL Fetch Need Backend?
### Short Answer:
**YES, for production use with CORS-protected APIs, you need a backend proxy.**
### Long Answer:
## Current Situation (Frontend Only)
### ✅ What Works Without Backend:
1. **Public APIs with CORS enabled**
- JSONPlaceholder
- GitHub API (public endpoints)
- Any API with `Access-Control-Allow-Origin: *`
2. **Same-origin requests**
- Your own domain's API
- Example: `dewe.dev/api/*`
### ❌ What Doesn't Work Without Backend:
1. **CORS-protected APIs**
- Your `api.starsender.online`
- Most private/commercial APIs
- APIs without CORS headers
2. **Secure API keys**
- Exposing keys in frontend = security risk
- Anyone can see your API keys in browser
---
## Why You Need Backend
### 1. CORS Bypass
```
❌ Frontend → api.starsender.online
Browser blocks: CORS error
✅ Frontend → Your Backend → api.starsender.online
No CORS (server-to-server)
```
### 2. API Key Security
```
❌ Frontend stores API key
Visible in browser console
Anyone can steal it
✅ Backend stores API key
Hidden from users
Secure environment variables
```
### 3. Rate Limiting & Caching
```
✅ Backend can:
- Cache responses
- Implement rate limiting
- Log requests
- Add analytics
```
---
## Backend Architecture Options
### Option 1: Simple Proxy (Recommended)
**Tech Stack:** Node.js + Express
```javascript
// server.js
const express = require('express');
const axios = require('axios');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
// Proxy endpoint
app.post('/api/proxy', async (req, res) => {
try {
const { url, method, headers, body } = req.body;
// Make request to target API
const response = await axios({
method: method || 'GET',
url: url,
headers: headers || {},
data: body || undefined
});
res.json(response.data);
} catch (error) {
res.status(error.response?.status || 500).json({
error: error.message,
details: error.response?.data
});
}
});
app.listen(3000, () => {
console.log('Proxy server running on port 3000');
});
```
**Frontend Update:**
```javascript
// In handleFetchData function
const response = await fetch('https://your-backend.com/api/proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: url,
method: advancedOptions?.method || 'GET',
headers: advancedOptions?.headers || {},
body: advancedOptions?.body
})
});
```
---
### Option 2: Serverless Functions
**Platform:** Vercel, Netlify, AWS Lambda
```javascript
// api/proxy.js (Vercel/Netlify)
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { url, method, headers, body } = req.body;
try {
const response = await fetch(url, {
method: method || 'GET',
headers: headers || {},
body: body ? JSON.stringify(body) : undefined
});
const data = await response.json();
res.status(200).json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
```
---
### Option 3: Full Backend with Auth
**For PRO user management:**
```javascript
// server.js with authentication
const express = require('express');
const jwt = require('jsonwebtoken');
const axios = require('axios');
const app = express();
app.use(express.json());
// Middleware to check PRO status
const checkProUser = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
// Check if user is PRO
if (req.user.tier !== 'pro') {
return res.status(403).json({
error: 'PRO feature',
message: 'Upgrade to PRO to use advanced URL fetch'
});
}
next();
} catch (error) {
res.status(401).json({ error: 'Unauthorized' });
}
};
// Protected proxy endpoint
app.post('/api/proxy', checkProUser, async (req, res) => {
const { url, method, headers, body } = req.body;
// Only PRO users can use non-GET methods
if (method !== 'GET' && req.user.tier !== 'pro') {
return res.status(403).json({
error: 'PRO feature required for ' + method
});
}
try {
const response = await axios({
method: method || 'GET',
url: url,
headers: headers || {},
data: body || undefined
});
res.json(response.data);
} catch (error) {
res.status(error.response?.status || 500).json({
error: error.message
});
}
});
app.listen(3000);
```
---
## Implementation Roadmap
### Phase 1: Basic Proxy (Week 1)
- [ ] Set up Node.js/Express server
- [ ] Create `/api/proxy` endpoint
- [ ] Deploy to Vercel/Netlify
- [ ] Update frontend to use proxy
- [ ] Test with your API
### Phase 2: Security (Week 2)
- [ ] Add API key validation
- [ ] Implement rate limiting
- [ ] Add request logging
- [ ] Set up error handling
- [ ] Add CORS whitelist
### Phase 3: PRO Features (Week 3-4)
- [ ] Set up user authentication (JWT)
- [ ] Create user database (PostgreSQL/MongoDB)
- [ ] Implement tier checking (FREE/PRO)
- [ ] Add payment integration (Stripe)
- [ ] Update frontend to send auth tokens
### Phase 4: Advanced Features (Week 5+)
- [ ] Request caching (Redis)
- [ ] Analytics dashboard
- [ ] Usage limits per tier
- [ ] Webhook support
- [ ] API key management
---
## Cost Estimation
### Free Tier Options:
- **Vercel**: 100GB bandwidth/month
- **Netlify**: 100GB bandwidth/month
- **Railway**: $5/month credit
- **Render**: Free tier available
### Paid Options:
- **Vercel Pro**: $20/month
- **AWS Lambda**: Pay per request (~$0.20 per 1M requests)
- **DigitalOcean**: $5/month VPS
- **Heroku**: $7/month
---
## Security Best Practices
### 1. Environment Variables
```bash
# .env
JWT_SECRET=your-secret-key
DATABASE_URL=postgresql://...
ALLOWED_ORIGINS=https://dewe.dev,http://localhost:3001
```
### 2. Rate Limiting
```javascript
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);
```
### 3. Input Validation
```javascript
const { body, validationResult } = require('express-validator');
app.post('/api/proxy', [
body('url').isURL(),
body('method').isIn(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']),
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// ... rest of code
});
```
---
## Summary
### Current State (Frontend Only):
✅ Works with public APIs
❌ Fails with CORS-protected APIs
❌ Can't secure API keys
❌ No PRO user management
### With Backend:
✅ Works with any API (no CORS issues)
✅ Secure API key storage
✅ PRO user authentication
✅ Rate limiting & caching
✅ Analytics & logging
✅ Production-ready
### Recommendation:
**Start with Option 1 (Simple Proxy)** for immediate CORS bypass, then gradually add authentication and PRO features in Phase 2-3.
**Estimated Timeline:** 2-4 weeks for full implementation with PRO features.

245
DOCUMENTATION_INDEX.md Normal file
View File

@@ -0,0 +1,245 @@
# Documentation Index
**Last Updated:** October 22, 2025
This document provides an overview of all project documentation and their purposes.
---
## 📚 Core Documentation
### **PROJECT_ROADMAP.md** 🎯
**Purpose:** High-level project vision, roadmap, and strategic planning
**Audience:** Project leads, stakeholders
**Contains:**
- Vision and goals
- Current status (8 active tools)
- Completed features (Markdown Editor, SEO, tool consolidation)
- Future phases (Diagram Tool, PRO tier, monetization)
- Success metrics and growth strategy
**Status:** ✅ Updated (Oct 22, 2025)
---
### **TODO.md** ✅
**Purpose:** Detailed task list with checkboxes for tracking progress
**Audience:** Developers, project managers
**Contains:**
- Phase 1: Foundation & Monetization (✅ Mostly complete)
- Phase 2: Content Expansion (✅ Markdown Editor done)
- Phase 3: Monetization Backend (Planned)
- Quick wins and metrics to track
**Status:** ✅ Updated (Oct 22, 2025)
---
## 🛠️ Technical Guides
### **EDITOR_TOOL_GUIDE.md** 📖
**Purpose:** Comprehensive guide for building new editor tools
**Audience:** Developers
**Contains:**
- Consistent UX patterns
- Input methods (Create/URL/Paste/Open)
- Data loss prevention
- Export patterns
- Code examples
**Status:** ✅ Active reference
---
### **EDITOR_CHECKLIST.md** ☑️
**Purpose:** Quality checklist for new tools
**Audience:** Developers, QA
**Contains:**
- Feature completeness checklist
- UX consistency checks
- Testing requirements
- Documentation requirements
**Status:** ✅ Active reference
---
### **FEATURE_TOGGLE_GUIDE.md** 🎛️
**Purpose:** Guide for implementing feature toggles
**Audience:** Developers
**Contains:**
- Feature flag patterns
- PRO tier features
- A/B testing setup
- Configuration management
**Status:** ✅ Active reference
---
## 💰 Monetization Documentation
### **ADSENSE_REVISED_STRATEGY.md** 📊
**Purpose:** Current AdSense implementation strategy
**Audience:** Project leads, developers
**Contains:**
- Clean homepage strategy (no ads)
- Tool pages monetization (3 desktop ads, 1 mobile)
- Revenue projections
- Implementation details
- **This is the ACTIVE strategy**
**Status:** ✅ Current strategy
---
### **ADSENSE_SETUP_GUIDE.md** 🔧
**Purpose:** Step-by-step AdSense setup instructions
**Audience:** Developers, admins
**Contains:**
- Account setup steps
- Ad unit creation
- Code implementation
- Testing procedures
**Status:** ✅ Active guide
---
### **ADSENSE_STRATEGY.md** 📝
**Purpose:** Original AdSense strategy (with homepage ads)
**Audience:** Reference only
**Contains:**
- Alternative strategy with homepage ads
- Comparison with revised strategy
- **NOT the current strategy**
**Status:** ⚠️ Reference only (superseded by REVISED)
---
### **BACKEND_REQUIREMENTS.md** 🖥️
**Purpose:** Backend requirements for PRO tier
**Audience:** Backend developers
**Contains:**
- Authentication system
- Payment integration
- CORS proxy service
- Database schema
- API endpoints
**Status:** 📅 Future reference (Phase 3)
---
## 🔍 SEO Documentation
### **SEO_IMPROVEMENT_PLAN.md** 🚀
**Purpose:** Comprehensive SEO strategy and implementation guide
**Audience:** Developers, marketers
**Contains:**
- Priority 1: Critical improvements (✅ DONE)
- Sitemap updates
- FAQ schema
- Breadcrumb schema
- Internal linking
- Priority 2-6: Ongoing improvements
- Long-term strategy
- Expected results
**Status:** ✅ Partially complete (Priority 1 done)
---
## 📖 User Documentation
### **README.md** 📄
**Purpose:** Project overview and setup instructions
**Audience:** Developers, contributors
**Contains:**
- Project description
- Installation instructions
- Development setup
- Deployment guide
**Status:** ✅ Active
---
## 🗑️ Removed Documentation
The following documents were **removed** as they were redundant or completed:
-`MARKDOWN_EDITOR_TASKS.md` - Merged into TODO.md
-`MARKDOWN_EDITOR_PLAN.md` - Merged into PROJECT_ROADMAP.md
-`MARKDOWN_EDITOR_ANALYSIS.md` - No longer needed (completed)
-`NEXT_TASK_RECOMMENDATION.md` - Outdated (Markdown Editor done)
-`SEO_FIX_GUIDE.md` - Merged into SEO_IMPROVEMENT_PLAN.md
-`SEO_QUICK_WINS.md` - Merged into SEO_IMPROVEMENT_PLAN.md
-`ADVANCED_URL_FETCH_FIXES.md` - Completed, no longer needed
-`CORS_AND_IMPROVEMENTS.md` - Completed, no longer needed
---
## 🎯 Quick Reference
### **What to Read First:**
1. **PROJECT_ROADMAP.md** - Understand the vision
2. **TODO.md** - See what's next
3. **EDITOR_TOOL_GUIDE.md** - Learn the patterns
### **Building a New Tool:**
1. Read **EDITOR_TOOL_GUIDE.md**
2. Follow **EDITOR_CHECKLIST.md**
3. Update **TODO.md** when done
### **Implementing AdSense:**
1. Read **ADSENSE_REVISED_STRATEGY.md** (current strategy)
2. Follow **ADSENSE_SETUP_GUIDE.md**
3. Ignore **ADSENSE_STRATEGY.md** (old strategy)
### **SEO Improvements:**
1. Check **SEO_IMPROVEMENT_PLAN.md**
2. Priority 1 is ✅ DONE
3. Work on Priority 2-6 as needed
---
## 📊 Current Project Status
### ✅ Completed (October 22, 2025):
- **8 Active Tools** (Markdown Editor, Object Editor, Table Editor, Invoice Editor, URL/Base64/Beautifier/Diff/Text Length)
- **Tool Consolidation** (Merged JSON, Serialize, CSV-JSON into main editors)
- **SEO Optimization** (FAQ schema, breadcrumbs, internal linking, sitemap)
- **Related Tools** (Recommendations on each tool page)
- **Custom 404 Page** (With tool suggestions)
- **Ad Infrastructure** (Placeholder ads ready)
### ⏳ In Progress:
- **AdSense Approval** (Awaiting Google review)
### 📅 Next Up:
- **Diagram Tool** (After AdSense approval)
- **PRO Tier** (Backend, auth, payment)
- **More Small Tools** (Hash, UUID, Timestamp, Color, etc.)
---
## 🔄 Document Maintenance
### When to Update:
- **PROJECT_ROADMAP.md**: When completing major phases or changing strategy
- **TODO.md**: Mark tasks as done, add new tasks
- **SEO_IMPROVEMENT_PLAN.md**: After implementing SEO improvements
- **ADSENSE_REVISED_STRATEGY.md**: When changing ad strategy or getting new data
### Archive Policy:
- Completed task-specific docs → Delete
- Outdated strategies → Keep for reference, mark as "Reference Only"
- Active guides → Keep updated
---
**Single Source of Truth:** PROJECT_ROADMAP.md + TODO.md
**Everything else:** Supporting documentation

198
FEATURE_TOGGLE_GUIDE.md Normal file
View File

@@ -0,0 +1,198 @@
# Feature Toggle System - FREE vs PRO
## Overview
The Advanced URL Fetch feature is now a **PRO feature** with a toggle system that supports FREE and PRO tiers.
## Files Created
### 1. `/src/config/features.js`
**Purpose:** Central configuration for all FREE/PRO features
**Key Functions:**
- `getCurrentUserTier()` - Returns current user tier (static for now, will be dynamic)
- `isFeatureEnabled(featureName)` - Check if a feature is enabled for current user
- `isProUser()` - Quick check if user is PRO
- `getFeatureInfo(featureName)` - Get feature metadata
**Current Features:**
- `ADVANCED_URL_FETCH` - PRO only
- `BULK_OPERATIONS` - PRO only (placeholder)
- `EXPORT_TEMPLATES` - PRO only (placeholder)
- `CLOUD_SYNC` - PRO only (placeholder)
**How to Toggle for Testing:**
```javascript
// In /src/config/features.js, line ~15
const staticTier = USER_TIER.FREE; // Change to USER_TIER.PRO to test
```
### 2. `/src/components/ProBadge.js`
**Purpose:** Reusable Pro badge and feature lock components
**Components:**
- `<ProBadge />` - Shows PRO badge (3 variants: badge, button, inline)
- `<ProFeatureLock />` - Shows locked feature message with upgrade prompt
**Usage Examples:**
```jsx
// Simple badge
<ProBadge size="sm" />
// Upgrade button
<ProBadge variant="button" onClick={handleUpgrade} />
// Inline text
<ProBadge variant="inline" size="xs" />
// Feature lock message
<ProFeatureLock
featureName="Advanced URL Fetch"
featureDescription="Custom headers, auth, and more"
onUpgrade={handleUpgrade}
/>
```
### 3. `/src/components/AdvancedURLFetch.js` (Updated)
**Purpose:** URL fetch component with FREE/PRO support
**Changes:**
- Imports feature toggle system
- Shows PRO badge on "Show Advanced Options" button
- Displays `<ProFeatureLock />` when user is FREE tier
- Shows full advanced options when user is PRO tier
## User Experience
### FREE Tier Users:
1. See basic URL input with GET method only
2. See "Show Advanced Options" button with PRO badge
3. Click button → See feature lock message with upgrade prompt
4. Can still use basic URL fetch (current functionality)
### PRO Tier Users:
1. See basic URL input with method selector (GET/POST/PUT/DELETE/PATCH)
2. See "Show Advanced Options" button (no PRO badge)
3. Click button → See full advanced options:
- Query Parameters builder
- Custom Headers
- Authentication (Bearer, API Key, Basic Auth)
- Request Body editor
- Save/Load Presets
## Integration Guide
### In ObjectEditor or TableEditor:
```jsx
import AdvancedURLFetch from '../components/AdvancedURLFetch';
// In component:
const [showAdvanced, setShowAdvanced] = useState(false);
const handleUpgrade = () => {
// Navigate to pricing page or show upgrade modal
alert('Upgrade to PRO!');
};
// In JSX:
<AdvancedURLFetch
url={fetchUrl}
onUrlChange={setFetchUrl}
onFetch={handleFetchData}
fetching={fetching}
showAdvanced={showAdvanced}
onToggleAdvanced={() => setShowAdvanced(!showAdvanced)}
onUpgrade={handleUpgrade}
/>
```
## Future: Dynamic Tier from Database
When implementing authentication and database:
```javascript
// /src/config/features.js
export const getCurrentUserTier = () => {
// Replace static tier with:
const user = getUserFromAuth(); // Your auth function
return user?.tier || USER_TIER.FREE;
};
// Or with API call:
export const getCurrentUserTier = async () => {
const response = await fetch('/api/user/tier');
const data = await response.json();
return data.tier || USER_TIER.FREE;
};
```
## Adding New PRO Features
1. **Add to features.js:**
```javascript
export const FEATURES = {
// ... existing features
MY_NEW_FEATURE: {
name: 'My New Feature',
description: 'Description of what it does',
tier: USER_TIER.PRO,
enabled: (userTier) => userTier === USER_TIER.PRO
}
};
```
2. **Use in component:**
```jsx
import { isFeatureEnabled } from '../config/features';
const MyComponent = () => {
const isEnabled = isFeatureEnabled('MY_NEW_FEATURE');
return (
<div>
{isEnabled ? (
<ProFeature />
) : (
<ProFeatureLock featureName="My New Feature" />
)}
</div>
);
};
```
## Testing
### Test as FREE user:
1. Keep `staticTier = USER_TIER.FREE` in features.js
2. Run dev server: `npm start`
3. Go to Object Editor → URL tab
4. Click "Show Advanced Options"
5. Should see PRO lock message
### Test as PRO user:
1. Change to `staticTier = USER_TIER.PRO` in features.js
2. Restart dev server
3. Go to Object Editor → URL tab
4. Click "Show Advanced Options"
5. Should see full advanced options
## Benefits
**Clean Separation**: FREE and PRO features clearly separated
**Easy Testing**: Toggle tier with one line change
**Reusable Components**: ProBadge and ProFeatureLock can be used anywhere
**Future-Ready**: Easy to connect to real authentication/database
**User-Friendly**: Clear upgrade prompts with feature descriptions
**Maintainable**: All feature flags in one central location
## Next Steps
1. ✅ Feature toggle system created
2. ✅ Pro badge components created
3. ✅ AdvancedURLFetch updated with FREE/PRO support
4. ⏳ Integrate into ObjectEditor
5. ⏳ Integrate into TableEditor
6. ⏳ Create pricing/upgrade page
7. ⏳ Implement authentication system
8. ⏳ Connect to database for dynamic tier management

View File

@@ -1,6 +1,6 @@
# Developer Tools - Project Roadmap # Developer Tools - Project Roadmap
**Last Updated:** October 14, 2025 **Last Updated:** October 22, 2025
--- ---
@@ -16,10 +16,16 @@ Build a comprehensive suite of developer tools with a focus on:
## 📊 Current Status ## 📊 Current Status
### ✅ Completed Tools ### ✅ Completed Tools (8 Active Tools)
- **Object Editor** - JSON/PHP serialized data editor - **Markdown Editor** - Write & preview markdown with live rendering ✨ NEW
- **Table Editor** - Tabular data editor with multi-format support - **Object Editor** - JSON/PHP serialized data editor (merged JSON & Serialize tools)
- **Table Editor** - Tabular data editor with multi-format support (merged CSV-JSON)
- **Invoice Editor** - Professional invoice generator - **Invoice Editor** - Professional invoice generator
- **URL Encoder/Decoder** - Encode/decode URLs
- **Base64 Encoder/Decoder** - Base64 conversion
- **Code Beautifier** - Format & minify code
- **Diff Tool** - Compare text differences
- **Text Length Checker** - Text statistics
### ✅ Completed Infrastructure ### ✅ Completed Infrastructure
- Consistent input patterns (Create/URL/Paste/Open) - Consistent input patterns (Create/URL/Paste/Open)
@@ -27,6 +33,9 @@ Build a comprehensive suite of developer tools with a focus on:
- Data loss prevention (confirmation modals) - Data loss prevention (confirmation modals)
- Dark mode support - Dark mode support
- Responsive design - Responsive design
- SEO optimization (FAQ schema, breadcrumbs, internal linking)
- Related tools recommendations
- Custom 404 page
- Documentation (EDITOR_TOOL_GUIDE.md, EDITOR_CHECKLIST.md) - Documentation (EDITOR_TOOL_GUIDE.md, EDITOR_CHECKLIST.md)
--- ---
@@ -36,7 +45,7 @@ Build a comprehensive suite of developer tools with a focus on:
### Phase 1: Foundation & Monetization (Current - Week 1-2) ### Phase 1: Foundation & Monetization (Current - Week 1-2)
#### Priority 1: Advanced URL Fetch ⭐ #### Priority 1: Advanced URL Fetch ⭐
**Status:** Planned **Status:** ✅ Completed (Hidden for PRO tier)
**Timeline:** 1-2 days **Timeline:** 1-2 days
**Impact:** HIGH - Benefits all existing and future tools **Impact:** HIGH - Benefits all existing and future tools
@@ -68,7 +77,7 @@ Build a comprehensive suite of developer tools with a focus on:
--- ---
#### Priority 2: Ad Space Preparation 💰 #### Priority 2: Ad Space Preparation 💰
**Status:** Planned **Status:** ✅ Completed (Placeholder ads ready)
**Timeline:** 1 day **Timeline:** 1 day
**Impact:** HIGH - Start monetization immediately **Impact:** HIGH - Start monetization immediately
@@ -100,9 +109,9 @@ Build a comprehensive suite of developer tools with a focus on:
**Specifications:** **Specifications:**
- **Desktop:** - **Desktop:**
- 300px fixed width right column - 300px fixed width right column
- Sticky scroll (ads stay visible) - Fixed positioning (ads stay visible)
- 3 ad blocks maximum - 3 ad blocks maximum
- Ad sizes: 300x250, 300x600 - Ad sizes: All 300x250 (Medium Rectangle) - Google AdSense policy compliant
- Hide below 1200px viewport width - Hide below 1200px viewport width
- Main content: `calc(100% - 320px)` - Main content: `calc(100% - 320px)`
@@ -123,7 +132,7 @@ Build a comprehensive suite of developer tools with a focus on:
--- ---
#### Priority 3: AdSense Integration 💵 #### Priority 3: AdSense Integration 💵
**Status:** Planned **Status:** ⏳ In Progress (Awaiting approval)
**Timeline:** 1 day **Timeline:** 1 day
**Impact:** HIGH - Start earning revenue **Impact:** HIGH - Start earning revenue
@@ -138,7 +147,7 @@ Build a comprehensive suite of developer tools with a focus on:
**Ad Units Needed:** **Ad Units Needed:**
- Desktop Sidebar 1 (300x250) - Desktop Sidebar 1 (300x250)
- Desktop Sidebar 2 (300x250) - Desktop Sidebar 2 (300x250)
- Desktop Sidebar 3 (300x600) - Desktop Sidebar 3 (300x250)
- Mobile Bottom Banner (320x50) - Mobile Bottom Banner (320x50)
**Compliance:** **Compliance:**
@@ -152,7 +161,7 @@ Build a comprehensive suite of developer tools with a focus on:
### Phase 2: Content Expansion (Week 3-6) ### Phase 2: Content Expansion (Week 3-6)
#### Markdown Editor 📝 #### Markdown Editor 📝
**Status:** Planned **Status:** ✅ Completed (October 22, 2025)
**Timeline:** 1-2 weeks **Timeline:** 1-2 weeks
**Impact:** HIGH - Major new feature, attracts new users **Impact:** HIGH - Major new feature, attracts new users

View File

@@ -1,412 +0,0 @@
# SEO Fix Guide - Google Search Console Indexing
**Date:** October 15, 2025
**Issue:** Google Search Console only indexing homepage, not tool pages
**Status:** FIXED ✅
---
## 🔍 Problems Identified
### 1. **Outdated Sitemap**
- Missing Invoice Editor (`/invoice-editor`)
- Missing What's New page (`/whats-new`)
- Old lastmod dates (2025-01-23)
- Wrong priorities
### 2. **React SPA Issue** (CRITICAL)
- Google can't index JavaScript-rendered pages
- Tool pages have no HTML content for crawlers
- All content loads client-side via React Router
- Search engines see empty `<div id="root"></div>`
### 3. **Missing Meta Tags**
- No dynamic meta tags per page
- No Open Graph tags
- No Twitter Card tags
- No structured data (JSON-LD)
---
## ✅ Solutions Implemented
### 1. **Updated Sitemap.xml**
**File:** `/public/sitemap.xml`
**Changes:**
- ✅ Added Invoice Editor
- ✅ Added What's New page
- ✅ Updated all lastmod dates to 2025-10-15
- ✅ Increased editor tools priority to 0.9
- ✅ Added comments for better organization
- ✅ Proper priority hierarchy
### 2. **Pre-rendering with react-snap**
**File:** `package.json`
**Added:**
```json
{
"dependencies": {
"react-snap": "^1.23.0"
},
"scripts": {
"build": "react-scripts build && react-snap"
},
"reactSnap": {
"include": [
"/",
"/object-editor",
"/table-editor",
"/invoice-editor",
"/url",
"/base64",
"/beautifier",
"/diff",
"/text-length",
"/whats-new",
"/privacy",
"/terms"
]
}
}
```
**What it does:**
- Generates static HTML for each route
- Crawlers see full HTML content
- Improves SEO dramatically
- Faster first paint
### 3. **Dynamic Meta Tags with react-helmet-async**
**Files Created:**
- `/src/components/SEO.js` - Reusable SEO component
- Updated `/src/App.js` - Wrapped with HelmetProvider
- Updated `/src/pages/Home.js` - Added SEO component
**Features:**
- Dynamic title per page
- Dynamic description per page
- Open Graph tags (Facebook)
- Twitter Card tags
- JSON-LD structured data
- Canonical URLs
---
## 📋 Steps to Fix in Google Search Console
### Step 1: Verify Sitemap Update
1. **Go to Google Search Console**
- URL: https://search.google.com/search-console
- Select property: `dewe.dev`
2. **Navigate to Sitemaps**
- Left sidebar → "Sitemaps"
3. **Remove Old Sitemap** (if exists)
- Find `https://dewe.dev/sitemap.xml`
- Click the 3 dots → "Remove sitemap"
4. **Submit New Sitemap**
- Click "Add a new sitemap"
- Enter: `sitemap.xml`
- Click "Submit"
5. **Wait for Processing**
- Status will show "Couldn't fetch" initially
- After deployment, it will show "Success"
- Check back in 24-48 hours
---
### Step 2: Request Indexing for Key Pages
1. **Go to URL Inspection Tool**
- Top search bar in Google Search Console
2. **Inspect Each Tool Page:**
```
https://dewe.dev/object-editor
https://dewe.dev/table-editor
https://dewe.dev/invoice-editor
https://dewe.dev/url
https://dewe.dev/base64
https://dewe.dev/beautifier
https://dewe.dev/diff
https://dewe.dev/text-length
https://dewe.dev/whats-new
```
3. **For Each URL:**
- Paste URL in search bar
- Click "Test Live URL"
- Wait for test to complete
- If "URL is on Google": Great!
- If "URL is not on Google": Click "Request Indexing"
- Repeat for all pages
---
### Step 3: Check robots.txt
1. **Go to Settings**
- Left sidebar → "Settings"
- Click "Open report" under "robots.txt"
2. **Verify robots.txt is accessible**
- Should show your robots.txt content
- Should reference sitemap: `Sitemap: https://dewe.dev/sitemap.xml`
3. **If not accessible:**
- Check if `https://dewe.dev/robots.txt` works in browser
- Ensure Coolify/server serves static files correctly
---
### Step 4: Monitor Coverage
1. **Go to Coverage Report**
- Left sidebar → "Coverage" (or "Pages")
2. **Check Indexed Pages**
- Should see increase in "Valid" pages
- Monitor "Excluded" and "Error" sections
3. **Common Issues:**
- **"Discovered - currently not indexed"**: Normal, Google will index soon
- **"Crawled - currently not indexed"**: Low priority, may take time
- **"Excluded by 'noindex' tag"**: Check meta tags (shouldn't happen)
- **"Soft 404"**: Page has no content (pre-rendering should fix this)
---
### Step 5: Check Enhancements
1. **Go to Enhancements**
- Left sidebar → "Enhancements"
2. **Check Mobile Usability**
- Should show "Valid" for all pages
- Fix any mobile issues
3. **Check Core Web Vitals**
- Monitor performance metrics
- Aim for "Good" status
---
## 🚀 Deployment Steps
### 1. Install Dependencies
```bash
cd /Users/dwindown/CascadeProjects/developer-tools
npm install
```
### 2. Test Build Locally
```bash
npm run build
```
**Expected:**
- Build completes successfully
- react-snap generates HTML files for each route
- Check `build/` folder for HTML files
### 3. Deploy to Production
```bash
git add -A
git commit -m "fix: improve SEO with pre-rendering and dynamic meta tags
- Updated sitemap.xml with all current pages
- Added react-snap for static HTML generation
- Implemented react-helmet-async for dynamic meta tags
- Created SEO component with Open Graph and Twitter Cards
- Added JSON-LD structured data
- Fixed Google Search Console indexing issues"
git push
```
### 4. Verify Deployment
- Wait for Coolify to deploy
- Check https://dewe.dev/sitemap.xml
- Check https://dewe.dev/robots.txt
- Check https://dewe.dev/object-editor (view source, should see HTML content)
---
## 🧪 Testing & Verification
### Test 1: View Page Source
1. Open https://dewe.dev/object-editor
2. Right-click → "View Page Source"
3. **Before fix:** Only see `<div id="root"></div>`
4. **After fix:** See full HTML content with meta tags
### Test 2: Google Rich Results Test
1. Go to https://search.google.com/test/rich-results
2. Enter: `https://dewe.dev/object-editor`
3. Should show structured data (JSON-LD)
4. Should pass validation
### Test 3: Facebook Sharing Debugger
1. Go to https://developers.facebook.com/tools/debug/
2. Enter: `https://dewe.dev/object-editor`
3. Should show Open Graph tags
4. Should display preview image
### Test 4: Twitter Card Validator
1. Go to https://cards-dev.twitter.com/validator
2. Enter: `https://dewe.dev/object-editor`
3. Should show Twitter Card preview
4. Should display correctly
---
## 📊 Expected Timeline
| Action | Timeline |
|--------|----------|
| Deploy changes | Immediate |
| Sitemap processed | 1-2 hours |
| Pages crawled | 1-7 days |
| Pages indexed | 3-14 days |
| Full coverage | 2-4 weeks |
**Note:** Google indexing is not instant. Be patient!
---
## 🔧 Troubleshooting
### Issue: "Couldn't fetch sitemap"
**Solution:**
- Check if `https://dewe.dev/sitemap.xml` is accessible
- Ensure Coolify serves static files from `/public`
- Check server logs for 404 errors
### Issue: "Discovered - currently not indexed"
**Solution:**
- Normal! Google discovered but hasn't indexed yet
- Request indexing manually (Step 2 above)
- Wait 7-14 days
### Issue: "Crawled - currently not indexed"
**Solution:**
- Google crawled but deemed low priority
- Improve content quality
- Add more internal links
- Wait for Google to re-evaluate
### Issue: "Soft 404"
**Solution:**
- Page has no content or very little content
- Pre-rendering should fix this
- Check if react-snap generated HTML correctly
- Verify build output
### Issue: react-snap fails during build
**Solution:**
- Check console for errors
- May need to add `window.snapSaveState = () => ({})` to index.js
- Try `npm run build:no-snap` to build without pre-rendering
- Check react-snap documentation
---
## 📝 Additional Recommendations
### 1. Add More Content
- Write blog posts about each tool
- Create tutorial pages
- Add FAQ section
- More content = better SEO
### 2. Internal Linking
- Link between related tools
- Add "Related Tools" section
- Create tool categories pages
- Improve navigation
### 3. Performance Optimization
- Optimize images
- Minimize JavaScript
- Use CDN for assets
- Improve Core Web Vitals
### 4. Schema Markup
- Add more structured data
- Use SoftwareApplication schema
- Add BreadcrumbList schema
- Add Organization schema
### 5. Social Signals
- Share on social media
- Get backlinks
- Engage with developer communities
- Build brand awareness
---
## 🎯 Success Metrics
### Week 1
- ✅ Sitemap processed
- ✅ 3-5 pages indexed
- ✅ No crawl errors
### Week 2
- ✅ 8-10 pages indexed
- ✅ Appearing in search results
- ✅ Mobile usability: Good
### Week 4
- ✅ All pages indexed
- ✅ Ranking for brand keywords
- ✅ Organic traffic increasing
---
## 📞 Support
If issues persist after 2 weeks:
1. Check Google Search Console for specific errors
2. Review server logs for crawl errors
3. Test with different browsers
4. Consider hiring SEO consultant
---
## ✅ Checklist
### Immediate Actions
- [ ] Deploy code changes
- [ ] Verify sitemap.xml is accessible
- [ ] Verify robots.txt is accessible
- [ ] Submit sitemap in Google Search Console
- [ ] Request indexing for key pages
### Within 24 Hours
- [ ] Check sitemap processing status
- [ ] Verify HTML pre-rendering works
- [ ] Test Open Graph tags
- [ ] Test Twitter Cards
### Within 1 Week
- [ ] Monitor coverage report
- [ ] Check for crawl errors
- [ ] Verify pages being indexed
- [ ] Check search appearance
### Within 1 Month
- [ ] Review all pages indexed
- [ ] Check ranking positions
- [ ] Monitor organic traffic
- [ ] Optimize based on data
---
**Good luck! Your SEO should improve significantly with these changes.** 🚀

620
SEO_IMPROVEMENT_PLAN.md Normal file
View File

@@ -0,0 +1,620 @@
# 🚀 SEO Improvement Plan for Developer Tools
## Current SEO Status: ✅ Good Foundation
**What's Already Implemented:**
- ✅ React Helmet for dynamic meta tags
- ✅ Canonical URLs
- ✅ Open Graph tags (Facebook)
- ✅ Twitter Cards
- ✅ JSON-LD structured data
- ✅ robots.txt
- ✅ sitemap.xml
- ✅ Responsive design
- ✅ Fast loading (React SPA)
---
## 🎯 Priority 1: Critical SEO Improvements (High Impact)
### 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
**Action:** Update `public/sitemap.xml`
```xml
<!-- Add these entries -->
<url>
<loc>https://dewe.dev/markdown-editor</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dewe.dev/json</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/csv-json</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/serialize</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/release-notes</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
```
**Impact:** 🔥 High - Google can't index pages it doesn't know about
---
### 2. **Create Blog/Tutorial Section**
**Why:** Fresh content = better rankings
**Implementation:**
```
/blog/
- how-to-format-json-online
- best-markdown-editor-features
- csv-to-json-conversion-guide
- developer-tools-productivity-tips
```
**Benefits:**
- Target long-tail keywords
- Build authority
- Internal linking opportunities
- Regular content updates signal active site
**Effort:** Medium | **Impact:** 🔥🔥 Very High
---
### 3. **Add FAQ Schema Markup**
**Current:** Only WebApplication schema
**Add:** FAQPage schema for each tool
**Example for Markdown Editor:**
```javascript
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [{
"@type": "Question",
"name": "Is this markdown editor free?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes, completely free. All processing happens in your browser."
}
}, {
"@type": "Question",
"name": "Does the markdown editor support GitHub Flavored Markdown?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes, full GFM support including tables, task lists, and syntax highlighting."
}
}]
}
```
**Impact:** 🔥🔥 High - Rich snippets in search results
---
### 4. **Implement Breadcrumb Schema**
**Add to each tool page:**
```javascript
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://dewe.dev"
}, {
"@type": "ListItem",
"position": 2,
"name": "Markdown Editor",
"item": "https://dewe.dev/markdown-editor"
}]
}
```
**Impact:** 🔥 Medium-High - Better SERP display
---
### 5. **Add HowTo Schema for Tool Pages**
**Example:**
```javascript
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "How to Convert Markdown to PDF",
"step": [{
"@type": "HowToStep",
"name": "Write Markdown",
"text": "Type or paste your markdown content"
}, {
"@type": "HowToStep",
"name": "Preview",
"text": "See live preview as you type"
}, {
"@type": "HowToStep",
"name": "Export",
"text": "Click PDF button to download"
}]
}
```
**Impact:** 🔥🔥 High - Featured snippets opportunity
---
## 🎯 Priority 2: Content & On-Page SEO (Medium-High Impact)
### 6. **Optimize Title Tags**
**Current:** Generic titles
**Improve:** Add power words and benefits
**Before:**
```
Markdown Editor | Developer Tools
```
**After:**
```
Free Markdown Editor with Live Preview & PDF Export | Developer Tools
```
**Pattern:**
- Include primary keyword
- Add benefit/feature
- Keep under 60 characters
- Use power words: Free, Best, Easy, Fast, Online
---
### 7. **Enhance Meta Descriptions**
**Current:** Generic descriptions
**Improve:** Action-oriented with CTAs
**Before:**
```
Write and preview markdown with live rendering
```
**After:**
```
✓ Free online markdown editor ✓ Live preview ✓ GitHub Flavored Markdown ✓ Export to PDF/HTML ✓ Syntax highlighting. Start writing now - no signup required!
```
**Tips:**
- Use checkmarks (✓) for features
- Include CTA
- 150-160 characters
- Front-load keywords
---
### 8. **Add H1 Tags to Tool Pages**
**Current:** Some pages missing proper H1
**Fix:** Ensure every page has ONE H1 with primary keyword
**Example:**
```jsx
<h1>Free Online Markdown Editor with Live Preview</h1>
```
---
### 9. **Internal Linking Strategy**
**Create link clusters:**
**Hub Page:** `/tools` (new page)
- Links to all tools
- Brief description of each
- Categories
**Tool Pages:**
- Link to related tools
- "You might also like" section
- Footer links to popular tools
**Example:**
```jsx
// At bottom of Markdown Editor
<div className="related-tools">
<h3>Related Tools</h3>
<ul>
<li><a href="/beautifier">Code Beautifier</a> - Format your code</li>
<li><a href="/text-length">Text Length Checker</a> - Count words</li>
</ul>
</div>
```
**Impact:** 🔥🔥 High - Distributes page authority
---
### 10. **Add Alt Text to All Images**
**Current:** Logo might be missing alt text
**Fix:** Add descriptive alt text
```jsx
<img
src="/logo.svg"
alt="Developer Tools - Free online utilities for web developers"
/>
```
---
## 🎯 Priority 3: Technical SEO (Medium Impact)
### 11. **Implement Lazy Loading for Images**
```jsx
<img
src="/og-image.png"
alt="Developer Tools"
loading="lazy"
/>
```
---
### 12. **Add Preconnect for External Resources**
**Add to `index.html`:**
```html
<link rel="preconnect" href="https://pagead2.googlesyndication.com">
<link rel="dns-prefetch" href="https://pagead2.googlesyndication.com">
```
---
### 13. **Create Custom 404 Page**
**Benefits:**
- Better UX
- Suggest related tools
- Keep users on site
- Internal linking opportunity
---
### 14. **Add Last Modified Dates**
**Show on each tool page:**
```jsx
<meta property="article:modified_time" content="2025-10-22T10:00:00Z" />
```
---
### 15. **Implement Service Worker for PWA**
**Benefits:**
- Offline functionality
- Faster repeat visits
- "Add to Home Screen"
- Google loves PWAs
---
## 🎯 Priority 4: Off-Page SEO & Marketing (High Impact)
### 16. **Submit to Developer Directories**
**Submit to:**
- ✅ Product Hunt
- ✅ Hacker News (Show HN)
- ✅ Reddit r/webdev, r/programming
- ✅ Dev.to
- ✅ Hashnode
- ✅ Free Code Camp
- ✅ Stack Overflow (answer questions, link to tools)
- ✅ Indie Hackers
- ✅ BetaList
- ✅ AlternativeTo
**Impact:** 🔥🔥🔥 Very High - Quality backlinks
---
### 17. **Create GitHub Repository**
**Benefits:**
- Backlink from GitHub
- Developer credibility
- Open source community
- Star count = social proof
**Add to footer:**
```jsx
<a href="https://github.com/yourusername/developer-tools">
Star on GitHub
</a>
```
---
### 18. **Guest Posting**
**Target blogs:**
- CSS-Tricks
- Smashing Magazine
- Dev.to
- Hashnode
- Medium
**Topics:**
- "10 Essential Developer Tools for 2025"
- "How to Boost Productivity with Online Tools"
- "Privacy-First Developer Tools"
---
### 19. **Create Tool Comparison Pages**
**Examples:**
- "Best Online JSON Editors Compared"
- "Markdown Editor vs. Notion vs. Typora"
- "Top 10 Free Developer Tools"
**Include your tools in comparisons**
---
### 20. **Build Email List**
**Add newsletter signup:**
```jsx
<div className="newsletter">
<h3>Get Tool Updates</h3>
<p>New features, tips, and productivity hacks</p>
<input type="email" placeholder="your@email.com" />
<button>Subscribe</button>
</div>
```
**Benefits:**
- Direct traffic
- Engagement signals
- Repeat visitors
---
## 🎯 Priority 5: Performance & Core Web Vitals
### 21. **Optimize Core Web Vitals**
**Check current scores:**
```bash
# Use Lighthouse
npm install -g lighthouse
lighthouse https://dewe.dev --view
```
**Target:**
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
**Improvements:**
- Code splitting
- Image optimization
- Font optimization
- Minimize JavaScript
---
### 22. **Enable Compression**
**Add to server config (if using custom server):**
```
gzip on;
gzip_types text/css application/javascript application/json;
```
---
### 23. **Implement CDN**
**Use Cloudflare or similar:**
- Faster global delivery
- DDoS protection
- SSL/TLS
- Caching
---
## 🎯 Priority 6: Local SEO (If Applicable)
### 24. **Add Organization Schema**
```javascript
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Developer Tools",
"url": "https://dewe.dev",
"logo": "https://dewe.dev/logo.svg",
"sameAs": [
"https://twitter.com/yourhandle",
"https://github.com/yourhandle"
]
}
```
---
## 📊 Measurement & Tracking
### 25. **Set Up Analytics**
**Implement:**
- Google Analytics 4
- Google Search Console
- Bing Webmaster Tools
- Hotjar (heatmaps)
- Plausible (privacy-friendly alternative)
**Track:**
- Organic traffic
- Keyword rankings
- Bounce rate
- Time on page
- Conversion rate (tool usage)
---
### 26. **Monitor Rankings**
**Tools:**
- Ahrefs
- SEMrush
- Moz
- Google Search Console
**Track keywords:**
- "online markdown editor"
- "json formatter"
- "csv to json converter"
- "developer tools"
- "code beautifier"
---
## 🎯 Quick Wins (Do First)
**Can implement today:**
1. Update sitemap.xml (add missing tools)
2. Improve title tags and meta descriptions
3. Add FAQ schema to 3 main tools
4. Submit to Product Hunt
5. Create GitHub repo
6. Add breadcrumb schema
7. Optimize images (add alt text)
8. Set up Google Search Console
9. Add internal links between tools
10. Create custom 404 page
**Estimated time:** 4-6 hours
**Expected impact:** 20-30% traffic increase in 2-3 months
---
## 📈 Long-Term Strategy (3-6 Months)
**Month 1:**
- Complete all Priority 1 tasks
- Submit to 10 directories
- Write 2 blog posts
- Set up analytics
**Month 2:**
- Complete Priority 2 tasks
- Write 4 blog posts
- Guest post on 2 sites
- Build 10 quality backlinks
**Month 3:**
- Complete Priority 3 tasks
- Write 4 blog posts
- Launch newsletter
- Optimize Core Web Vitals
**Month 4-6:**
- Continue content creation
- Build backlinks
- Optimize based on data
- A/B test landing pages
---
## 🎯 Expected Results
**After 3 months:**
- 50-100% increase in organic traffic
- Ranking for 20-30 keywords
- 500-1000 daily visitors
- 10-20 quality backlinks
**After 6 months:**
- 200-300% increase in organic traffic
- Ranking for 50-100 keywords
- 2000-3000 daily visitors
- 50+ quality backlinks
- Featured snippets for 5-10 queries
---
## 🚀 Next Steps
**Start with:**
1. Update sitemap.xml (30 min)
2. Improve meta tags (2 hours)
3. Add FAQ schema (2 hours)
4. Submit to Product Hunt (1 hour)
5. Set up Google Search Console (30 min)
**Total time:** ~6 hours
**ROI:** Very High
---
## 📚 Resources
**SEO Tools:**
- Google Search Console (free)
- Google Analytics (free)
- Ahrefs (paid)
- SEMrush (paid)
- Screaming Frog (free/paid)
**Learning:**
- Moz Beginner's Guide to SEO
- Google Search Central
- Ahrefs Blog
- Backlinko
**Communities:**
- r/SEO
- r/bigseo
- Indie Hackers
- Growth Hackers
---
**Remember:** SEO is a marathon, not a sprint. Consistency and quality content win in the long run! 🏆

15
TODO.md
View File

@@ -1,12 +1,12 @@
# Developer Tools - To-Do List # Developer Tools - To-Do List
**Last Updated:** October 14, 2025 **Last Updated:** October 22, 2025
--- ---
## 📋 Phase 1: Foundation & Monetization ## 📋 Phase 1: Foundation & Monetization
### ⭐ Priority 1: Advanced URL Fetch (1-2 days) ### ⭐ Priority 1: Advanced URL Fetch (1-2 days) - ✅ COMPLETED
#### Design & Planning #### Design & Planning
- [ ] Design UI mockup for simple mode - [ ] Design UI mockup for simple mode
@@ -108,7 +108,7 @@
--- ---
### 💰 Priority 2: Ad Space Preparation (1 day) ### 💰 Priority 2: Ad Space Preparation (1 day) - ✅ COMPLETED
#### Design #### Design
- [ ] Finalize desktop ad column layout - [ ] Finalize desktop ad column layout
@@ -123,8 +123,7 @@
- [ ] Space for 3 ad blocks - [ ] Space for 3 ad blocks
- [ ] Proper spacing between ads - [ ] Proper spacing between ads
- [ ] Create `AdBlock.jsx` component - [ ] Create `AdBlock.jsx` component
- [ ] Support 300x250 size - [ ] Support 300x250 size (all ads use this size for AdSense compliance)
- [ ] Support 300x600 size
- [ ] Placeholder content for testing - [ ] Placeholder content for testing
- [ ] Loading state - [ ] Loading state
- [ ] Error state (if ad fails to load) - [ ] Error state (if ad fails to load)
@@ -180,7 +179,7 @@
--- ---
### 💵 Priority 3: AdSense Integration (1 day) ### 💵 Priority 3: AdSense Integration (1 day) - ⏳ IN PROGRESS
#### AdSense Setup #### AdSense Setup
- [ ] Apply for Google AdSense account - [ ] Apply for Google AdSense account
@@ -193,7 +192,7 @@
- [ ] Log in to AdSense dashboard - [ ] Log in to AdSense dashboard
- [ ] Create ad unit: Desktop Sidebar 1 (300x250) - [ ] Create ad unit: Desktop Sidebar 1 (300x250)
- [ ] Create ad unit: Desktop Sidebar 2 (300x250) - [ ] Create ad unit: Desktop Sidebar 2 (300x250)
- [ ] Create ad unit: Desktop Sidebar 3 (300x600) - [ ] Create ad unit: Desktop Sidebar 3 (300x250)
- [ ] Create ad unit: Mobile Bottom Banner (320x50) - [ ] Create ad unit: Mobile Bottom Banner (320x50)
- [ ] Copy ad unit codes - [ ] Copy ad unit codes
@@ -261,7 +260,7 @@
## 📋 Phase 2: Content Expansion ## 📋 Phase 2: Content Expansion
### 📝 Markdown Editor - MVP (1-2 weeks) ### 📝 Markdown Editor - MVP (1-2 weeks) - ✅ COMPLETED (Oct 22, 2025)
#### Planning #### Planning
- [ ] Finalize feature list for MVP - [ ] Finalize feature list for MVP

90
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2", "@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.4.0",
"@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-sql": "^6.10.0",
"@codemirror/search": "^6.5.11", "@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2", "@codemirror/state": "^6.5.2",
@@ -28,11 +29,16 @@
"@uiw/react-codemirror": "^4.25.1", "@uiw/react-codemirror": "^4.25.1",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"dompurify": "^3.3.0",
"file-saver": "^2.0.5",
"highlight.js": "^11.11.1",
"html2pdf.js": "^0.12.1", "html2pdf.js": "^0.12.1",
"js-beautify": "^1.15.4", "js-beautify": "^1.15.4",
"jspdf": "^3.0.3", "jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2", "jspdf-autotable": "^5.0.2",
"lucide-react": "^0.540.0", "lucide-react": "^0.540.0",
"marked": "^16.4.1",
"marked-emoji": "^2.0.1",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
"react": "18.3.1", "react": "18.3.1",
"react-diff-view": "^3.3.2", "react-diff-view": "^3.3.2",
@@ -43,6 +49,7 @@
"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",
"turndown": "^7.2.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
@@ -2175,6 +2182,21 @@
"@lezer/json": "^1.0.0" "@lezer/json": "^1.0.0"
} }
}, },
"node_modules/@codemirror/lang-markdown": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.4.0.tgz",
"integrity": "sha512-ZeArR54seh4laFbUTVy0ZmQgO+C/cxxlW4jEoQMhL3HALScBpZBeZcLzrQmJsTEx4is9GzOe0bFAke2B1KZqeA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/markdown": "^1.0.0"
}
},
"node_modules/@codemirror/lang-sql": { "node_modules/@codemirror/lang-sql": {
"version": "6.10.0", "version": "6.10.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
@@ -3180,10 +3202,26 @@
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
}, },
"node_modules/@lezer/markdown": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.5.1.tgz",
"integrity": "sha512-F3ZFnIfNAOy/jPSk6Q0e3bs7e9grfK/n5zerkKoc5COH6Guy3Zb0vrJwXzdck79K16goBhYBRAvhf+ksqe0cMg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": { "node_modules/@marijn/find-cluster-break": {
"version": "1.0.2", "version": "1.0.2",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
"license": "BSD-2-Clause"
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1", "version": "5.1.1-v1",
"dev": true, "dev": true,
@@ -7496,11 +7534,10 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.2.7", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
"license": "(MPL-2.0 OR Apache-2.0)", "license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": { "optionalDependencies": {
"@types/trusted-types": "^2.0.7" "@types/trusted-types": "^2.0.7"
} }
@@ -8894,6 +8931,12 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
} }
}, },
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
"license": "MIT"
},
"node_modules/filelist": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
"dev": true, "dev": true,
@@ -9665,6 +9708,15 @@
"util-deprecate": "^1.0.2" "util-deprecate": "^1.0.2"
} }
}, },
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/hoopy": { "node_modules/hoopy": {
"version": "0.1.4", "version": "0.1.4",
"dev": true, "dev": true,
@@ -12141,6 +12193,27 @@
"tmpl": "1.0.5" "tmpl": "1.0.5"
} }
}, },
"node_modules/marked": {
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.1.tgz",
"integrity": "sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/marked-emoji": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-2.0.1.tgz",
"integrity": "sha512-P+nRr02dD+yPOFhtGdaVBzp0qzwlksI2f5GumIdHW/3UadzJ5sVi78CZikiSLr9PmdtUOZodZUBNIO6k38pDMQ==",
"license": "MIT",
"peerDependencies": {
"marked": ">=4 <17"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"dev": true, "dev": true,
@@ -17555,6 +17628,15 @@
"dev": true, "dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/turndown": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.1.tgz",
"integrity": "sha512-7YiPJw6rLClQL3oUKN3KgMaXeJJ2lAyZItclgKDurqnH61so4k4IH/qwmMva0zpuJc/FhRExBBnk7EbeFANlgQ==",
"license": "MIT",
"dependencies": {
"@mixmark-io/domino": "^2.2.0"
}
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"dev": true, "dev": true,

View File

@@ -10,6 +10,7 @@
"@codemirror/lang-html": "^6.4.9", "@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2", "@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.4.0",
"@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-sql": "^6.10.0",
"@codemirror/search": "^6.5.11", "@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2", "@codemirror/state": "^6.5.2",
@@ -24,11 +25,16 @@
"@uiw/react-codemirror": "^4.25.1", "@uiw/react-codemirror": "^4.25.1",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"dompurify": "^3.3.0",
"file-saver": "^2.0.5",
"highlight.js": "^11.11.1",
"html2pdf.js": "^0.12.1", "html2pdf.js": "^0.12.1",
"js-beautify": "^1.15.4", "js-beautify": "^1.15.4",
"jspdf": "^3.0.3", "jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2", "jspdf-autotable": "^5.0.2",
"lucide-react": "^0.540.0", "lucide-react": "^0.540.0",
"marked": "^16.4.1",
"marked-emoji": "^2.0.1",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
"react": "18.3.1", "react": "18.3.1",
"react-diff-view": "^3.3.2", "react-diff-view": "^3.3.2",
@@ -39,6 +45,7 @@
"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",
"turndown": "^7.2.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,8 +1,49 @@
{ {
"changelog": [ "changelog": [
{
"date": "2025-10-22",
"changes": [
{
"datetime": "2025-10-22T15:00:00+07:00",
"type": "feature",
"title": "New Markdown Editor with Live Preview",
"description": "Write and preview markdown in real-time with GitHub Flavored Markdown support, syntax highlighting for code blocks, and export to PDF, HTML, DOCX, or Plain Text. Perfect for creating README files, documentation, and blog posts."
},
{
"datetime": "2025-10-22T14:30:00+07:00",
"type": "enhancement",
"title": "Upgraded Code Editor with Expand/Collapse",
"description": "All paste fields now use a professional code editor with syntax highlighting, line numbers, and an expand/collapse toggle. Start with a compact 12-line view, then expand to full screen when you need more space. Works in Markdown Editor, Object Editor, Table Editor, and Invoice Editor."
},
{
"datetime": "2025-10-22T14:00:00+07:00",
"type": "enhancement",
"title": "Better Export Views with Syntax Highlighting",
"description": "Export sections in Table Editor and Object Editor now show your data with beautiful syntax highlighting and proper formatting. JSON, CSV, TSV, and SQL outputs are easier to read and verify before downloading."
},
{
"datetime": "2025-10-22T13:30:00+07:00",
"type": "enhancement",
"title": "Improved Object Editor with Advanced URL Fetching",
"description": "Object Editor now has smarter URL fetching with automatic content extraction. Fetch JSON from APIs or extract article content from HTML pages with quality indicators, metadata, word count, and reading time. Perfect for analyzing web content or API responses."
},
{
"datetime": "2025-10-22T13:00:00+07:00",
"type": "enhancement",
"title": "Object Editor Preview Mode",
"description": "New Preview/Edit toggle in Tree View lets you view data in read-only mode with full text visibility - perfect for long values. Click any nested JSON or serialized data directly in preview mode to explore its structure without switching to edit mode."
}
]
},
{ {
"date": "2025-10-15", "date": "2025-10-15",
"changes": [ "changes": [
{
"datetime": "2025-10-15T23:23:00+07:00",
"type": "feature",
"title": "Advanced URL Fetching with Content Extraction",
"description": "Object Editor now intelligently fetches and extracts content from any URL. Automatically detects if URL is a JSON API or HTML page. For HTML pages, extracts article content with quality indicators (Rich Article, General Content, etc.). Shows structured data including title, metadata, word count, and reading time. Perfect for analyzing web articles, blog posts, or API responses."
},
{ {
"datetime": "2025-10-15T22:32:00+07:00", "datetime": "2025-10-15T22:32:00+07:00",
"type": "enhancement", "type": "enhancement",

View File

@@ -13,6 +13,10 @@
<meta name="description" content="Developer Tools MVP - Essential utilities for web developers" /> <meta name="description" content="Developer Tools MVP - Essential utilities for web developers" />
<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 -->
<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>

View File

@@ -6,7 +6,7 @@
<!-- Homepage --> <!-- Homepage -->
<url> <url>
<loc>https://dewe.dev/</loc> <loc>https://dewe.dev/</loc>
<lastmod>2025-10-15</lastmod> <lastmod>2025-10-22</lastmod>
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>1.0</priority> <priority>1.0</priority>
</url> </url>
@@ -14,39 +14,45 @@
<!-- Editor Tools (High Priority) --> <!-- Editor Tools (High Priority) -->
<url> <url>
<loc>https://dewe.dev/object-editor</loc> <loc>https://dewe.dev/object-editor</loc>
<lastmod>2025-10-15</lastmod> <lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.9</priority> <priority>0.9</priority>
</url> </url>
<url> <url>
<loc>https://dewe.dev/table-editor</loc> <loc>https://dewe.dev/table-editor</loc>
<lastmod>2025-10-15</lastmod> <lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.9</priority> <priority>0.9</priority>
</url> </url>
<url> <url>
<loc>https://dewe.dev/invoice-editor</loc> <loc>https://dewe.dev/invoice-editor</loc>
<lastmod>2025-10-15</lastmod> <lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dewe.dev/markdown-editor</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.9</priority> <priority>0.9</priority>
</url> </url>
<!-- Converter Tools --> <!-- Encoder Tools -->
<url> <url>
<loc>https://dewe.dev/url</loc> <loc>https://dewe.dev/url</loc>
<lastmod>2025-10-15</lastmod> <lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>
<url> <url>
<loc>https://dewe.dev/base64</loc> <loc>https://dewe.dev/base64</loc>
<lastmod>2025-10-15</lastmod> <lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>
<url> <url>
<loc>https://dewe.dev/beautifier</loc> <loc>https://dewe.dev/beautifier</loc>
<lastmod>2025-10-15</lastmod> <lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>
@@ -54,13 +60,13 @@
<!-- Utility Tools --> <!-- Utility Tools -->
<url> <url>
<loc>https://dewe.dev/diff</loc> <loc>https://dewe.dev/diff</loc>
<lastmod>2025-10-15</lastmod> <lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>
<url> <url>
<loc>https://dewe.dev/text-length</loc> <loc>https://dewe.dev/text-length</loc>
<lastmod>2025-10-15</lastmod> <lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.8</priority> <priority>0.8</priority>
</url> </url>
@@ -68,7 +74,13 @@
<!-- Info Pages --> <!-- Info Pages -->
<url> <url>
<loc>https://dewe.dev/whats-new</loc> <loc>https://dewe.dev/whats-new</loc>
<lastmod>2025-10-15</lastmod> <lastmod>2025-10-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://dewe.dev/release-notes</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.7</priority> <priority>0.7</priority>
</url> </url>

View File

@@ -4,22 +4,21 @@ 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 Home from './pages/Home';
import JsonTool from './pages/JsonTool';
import SerializeTool from './pages/SerializeTool';
import UrlTool from './pages/UrlTool'; import UrlTool from './pages/UrlTool';
import Base64Tool from './pages/Base64Tool'; import Base64Tool from './pages/Base64Tool';
import CsvJsonTool from './pages/CsvJsonTool';
import BeautifierTool from './pages/BeautifierTool'; import BeautifierTool from './pages/BeautifierTool';
import DiffTool from './pages/DiffTool'; import DiffTool from './pages/DiffTool';
import TextLengthTool from './pages/TextLengthTool'; import TextLengthTool from './pages/TextLengthTool';
import ObjectEditor from './pages/ObjectEditor'; import ObjectEditor from './pages/ObjectEditor';
import TableEditor from './pages/TableEditor'; import TableEditor from './pages/TableEditor';
import InvoiceEditor from './pages/InvoiceEditor'; import InvoiceEditor from './pages/InvoiceEditor';
import MarkdownEditor from './pages/MarkdownEditor';
import InvoicePreview from './pages/InvoicePreview'; import InvoicePreview from './pages/InvoicePreview';
import InvoicePreviewMinimal from './pages/InvoicePreviewMinimal'; import InvoicePreviewMinimal from './pages/InvoicePreviewMinimal';
import ReleaseNotes from './pages/ReleaseNotes'; import ReleaseNotes from './pages/ReleaseNotes';
import TermsOfService from './pages/TermsOfService'; import TermsOfService from './pages/TermsOfService';
import PrivacyPolicy from './pages/PrivacyPolicy'; 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';
@@ -37,23 +36,22 @@ function App() {
<Layout> <Layout>
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/json" element={<JsonTool />} />
<Route path="/serialize" element={<SerializeTool />} />
<Route path="/url" element={<UrlTool />} /> <Route path="/url" element={<UrlTool />} />
<Route path="/base64" element={<Base64Tool />} /> <Route path="/base64" element={<Base64Tool />} />
<Route path="/csv-json" element={<CsvJsonTool />} />
<Route path="/beautifier" element={<BeautifierTool />} /> <Route path="/beautifier" element={<BeautifierTool />} />
<Route path="/diff" element={<DiffTool />} /> <Route path="/diff" element={<DiffTool />} />
<Route path="/text-length" element={<TextLengthTool />} /> <Route path="/text-length" element={<TextLengthTool />} />
<Route path="/object-editor" element={<ObjectEditor />} /> <Route path="/object-editor" element={<ObjectEditor />} />
<Route path="/table-editor" element={<TableEditor />} /> <Route path="/table-editor" element={<TableEditor />} />
<Route path="/invoice-editor" element={<InvoiceEditor />} /> <Route path="/invoice-editor" element={<InvoiceEditor />} />
<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={<ReleaseNotes />} />
<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 />} />
</Routes> </Routes>
</Layout> </Layout>
</Router> </Router>

36
src/components/AdBlock.js Normal file
View File

@@ -0,0 +1,36 @@
import React, { useEffect } from 'react';
/**
* 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);
}
}, []);
const [width, height] = size.split('x');
return (
<div className={`bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${className}`}>
<ins
className="adsbygoogle"
style={{
display: 'block',
width: `${width}px`,
height: `${height}px`
}}
data-ad-client="ca-pub-8644544686212757"
data-ad-slot={slot}
data-ad-format="fixed"
/>
</div>
);
};
export default AdBlock;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import AdBlock from './AdBlock';
/**
* 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 (
<aside className="hidden xl:block w-[300px] flex-shrink-0">
<div className="fixed top-20 right-8 w-[300px] space-y-5">
{/* Ad 1: Medium Rectangle */}
<AdBlock slot={slot1} size="300x250" />
{/* Ad 2: Medium Rectangle */}
<AdBlock slot={slot2} size="300x250" />
{/* Ad 3: Medium Rectangle */}
<AdBlock slot={slot3} size="300x250" />
</div>
</aside>
);
};
export default AdColumn;

View File

@@ -0,0 +1,531 @@
import React, { useState, useEffect } from 'react';
import { ChevronDown, ChevronUp, Plus, X, Save, FolderOpen, Braces, Code } from 'lucide-react';
import ProBadge, { ProFeatureLock } from './ProBadge';
import { isFeatureEnabled, getFeatureInfo } from '../config/features';
import CodeMirrorEditor from './CodeMirrorEditor';
import StructuredEditor from './StructuredEditor';
const AdvancedURLFetch = ({
url,
onUrlChange,
onFetch,
fetching,
showAdvanced = false,
onToggleAdvanced,
onUpgrade = null, // Callback for upgrade action
onEditBodyVisually = null // Callback to switch to visual editor
}) => {
const isProFeatureEnabled = isFeatureEnabled('ADVANCED_URL_FETCH');
const featureInfo = getFeatureInfo('ADVANCED_URL_FETCH');
const [method, setMethod] = useState('GET');
const [headers, setHeaders] = useState([{ key: '', value: '', enabled: true }]);
const [body, setBody] = useState('');
const [bodyViewMode, setBodyViewMode] = useState('raw'); // 'raw' or 'visual'
const [bodyStructuredData, setBodyStructuredData] = useState({});
const [queryParams, setQueryParams] = useState([{ key: '', value: '', enabled: true }]);
const [authType, setAuthType] = useState('none');
const [authToken, setAuthToken] = useState('');
const [authUsername, setAuthUsername] = useState('');
const [authPassword, setAuthPassword] = useState('');
const [presetName, setPresetName] = useState('');
const [savedPresets, setSavedPresets] = useState([]);
// Load saved presets from localStorage
useEffect(() => {
const saved = localStorage.getItem('urlFetchPresets');
if (saved) {
try {
setSavedPresets(JSON.parse(saved));
} catch (e) {
console.error('Failed to load presets:', e);
}
}
}, []);
// Add header row
const addHeader = () => {
setHeaders([...headers, { key: '', value: '', enabled: true }]);
};
// Remove header row
const removeHeader = (index) => {
setHeaders(headers.filter((_, i) => i !== index));
};
// Update header
const updateHeader = (index, field, value) => {
const newHeaders = [...headers];
newHeaders[index][field] = value;
setHeaders(newHeaders);
};
// Add query param row
const addQueryParam = () => {
setQueryParams([...queryParams, { key: '', value: '', enabled: true }]);
};
// Remove query param row
const removeQueryParam = (index) => {
setQueryParams(queryParams.filter((_, i) => i !== index));
};
// Update query param
const updateQueryParam = (index, field, value) => {
const newParams = [...queryParams];
newParams[index][field] = value;
setQueryParams(newParams);
};
// Build final URL with query params
const buildFinalUrl = () => {
const baseUrl = url.trim();
const enabledParams = queryParams.filter(p => p.enabled && p.key.trim());
if (enabledParams.length === 0) return baseUrl;
const urlObj = new URL(baseUrl.startsWith('http') ? baseUrl : 'https://' + baseUrl);
enabledParams.forEach(param => {
urlObj.searchParams.append(param.key, param.value);
});
return urlObj.toString();
};
// Build headers object
const buildHeaders = () => {
const headersObj = {};
// Add custom headers
headers
.filter(h => h.enabled && h.key.trim())
.forEach(h => {
headersObj[h.key] = h.value;
});
// Add auth headers
if (authType === 'bearer' && authToken.trim()) {
headersObj['Authorization'] = `Bearer ${authToken}`;
} else if (authType === 'apikey' && authToken.trim()) {
headersObj['X-API-Key'] = authToken;
} else if (authType === 'basic' && authUsername.trim()) {
const credentials = btoa(`${authUsername}:${authPassword}`);
headersObj['Authorization'] = `Basic ${credentials}`;
}
// Add content-type for POST/PUT/PATCH with body
if (['POST', 'PUT', 'PATCH'].includes(method) && body.trim() && !headersObj['Content-Type']) {
headersObj['Content-Type'] = 'application/json';
}
return headersObj;
};
// Handle fetch with advanced options
const handleAdvancedFetch = () => {
const finalUrl = buildFinalUrl();
const finalHeaders = buildHeaders();
const finalBody = ['POST', 'PUT', 'PATCH'].includes(method) && body.trim() ? body : undefined;
onFetch({
url: finalUrl,
method,
headers: finalHeaders,
body: finalBody
});
};
// Save preset
const savePreset = () => {
if (!presetName.trim()) {
alert('Please enter a preset name');
return;
}
const preset = {
name: presetName,
url,
method,
headers: headers.filter(h => h.key.trim()),
body,
queryParams: queryParams.filter(p => p.key.trim()),
authType,
authToken,
authUsername,
// Don't save password for security
timestamp: Date.now()
};
const newPresets = [...savedPresets, preset];
setSavedPresets(newPresets);
localStorage.setItem('urlFetchPresets', JSON.stringify(newPresets));
setPresetName('');
alert('Preset saved!');
};
// Load preset
const loadPreset = (preset) => {
onUrlChange(preset.url);
setMethod(preset.method);
setHeaders(preset.headers.length > 0 ? preset.headers : [{ key: '', value: '', enabled: true }]);
setBody(preset.body || '');
setQueryParams(preset.queryParams.length > 0 ? preset.queryParams : [{ key: '', value: '', enabled: true }]);
setAuthType(preset.authType);
setAuthToken(preset.authToken || '');
setAuthUsername(preset.authUsername || '');
};
// Delete preset
const deletePreset = (index) => {
// eslint-disable-next-line no-restricted-globals
if (confirm('Delete this preset?')) {
const newPresets = savedPresets.filter((_, i) => i !== index);
setSavedPresets(newPresets);
localStorage.setItem('urlFetchPresets', JSON.stringify(newPresets));
}
};
return (
<div className="space-y-3">
{/* Basic URL Input */}
<div className="flex gap-2">
<select
value={method}
onChange={(e) => setMethod(e.target.value)}
className="tool-input w-24"
disabled={!showAdvanced || !isProFeatureEnabled}
title={!isProFeatureEnabled ? "PRO feature - Upgrade to use other methods" : ""}
>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
<div className="relative flex-1">
<input
type="url"
value={url}
onChange={(e) => onUrlChange(e.target.value)}
placeholder="https://api.example.com/endpoint"
className="tool-input w-full"
onKeyPress={(e) => e.key === 'Enter' && (showAdvanced ? handleAdvancedFetch() : onFetch())}
/>
</div>
<button
onClick={showAdvanced ? handleAdvancedFetch : onFetch}
disabled={fetching || !url.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
>
{fetching ? 'Fetching...' : 'Fetch Data'}
</button>
</div>
{/* Toggle Advanced */}
{/* Advanced Options Toggle - Hidden for now */}
{false && (
<button
onClick={onToggleAdvanced}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-2"
>
{showAdvanced ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
{showAdvanced ? 'Hide' : 'Show'} Advanced Options
{!isProFeatureEnabled && <ProBadge size="xs" />}
</button>
)}
{/* Advanced Options */}
{showAdvanced && (
isProFeatureEnabled ? (
<div className="space-y-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
{/* Query Parameters */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Query Parameters
</label>
<button
onClick={addQueryParam}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
>
<Plus className="h-3 w-3" /> Add
</button>
</div>
<div className="space-y-2">
{queryParams.map((param, index) => (
<div key={index} className="flex gap-2 items-center">
<input
type="checkbox"
checked={param.enabled}
onChange={(e) => updateQueryParam(index, 'enabled', e.target.checked)}
className="w-4 h-4"
/>
<input
type="text"
value={param.key}
onChange={(e) => updateQueryParam(index, 'key', e.target.value)}
placeholder="key"
className="tool-input flex-1 text-sm"
/>
<input
type="text"
value={param.value}
onChange={(e) => updateQueryParam(index, 'value', e.target.value)}
placeholder="value"
className="tool-input flex-1 text-sm"
/>
<button
onClick={() => removeQueryParam(index)}
className="text-red-600 hover:text-red-700 dark:text-red-400"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
</div>
{/* Headers */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Headers
</label>
<button
onClick={addHeader}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
>
<Plus className="h-3 w-3" /> Add
</button>
</div>
<div className="space-y-2">
{headers.map((header, index) => (
<div key={index} className="flex gap-2 items-center">
<input
type="checkbox"
checked={header.enabled}
onChange={(e) => updateHeader(index, 'enabled', e.target.checked)}
className="w-4 h-4"
/>
<input
type="text"
value={header.key}
onChange={(e) => updateHeader(index, 'key', e.target.value)}
placeholder="Header-Name"
className="tool-input flex-1 text-sm"
/>
<input
type="text"
value={header.value}
onChange={(e) => updateHeader(index, 'value', e.target.value)}
placeholder="value"
className="tool-input flex-1 text-sm"
/>
<button
onClick={() => removeHeader(index)}
className="text-red-600 hover:text-red-700 dark:text-red-400"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
</div>
{/* Authentication */}
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 block mb-2">
Authentication
</label>
<select
value={authType}
onChange={(e) => setAuthType(e.target.value)}
className="tool-input w-full mb-2"
>
<option value="none">No Auth</option>
<option value="bearer">Bearer Token</option>
<option value="apikey">API Key</option>
<option value="basic">Basic Auth</option>
</select>
{authType === 'bearer' && (
<input
type="text"
value={authToken}
onChange={(e) => setAuthToken(e.target.value)}
placeholder="Enter bearer token"
className="tool-input w-full"
/>
)}
{authType === 'apikey' && (
<input
type="text"
value={authToken}
onChange={(e) => setAuthToken(e.target.value)}
placeholder="Enter API key"
className="tool-input w-full"
/>
)}
{authType === 'basic' && (
<div className="space-y-2">
<input
type="text"
value={authUsername}
onChange={(e) => setAuthUsername(e.target.value)}
placeholder="Username"
className="tool-input w-full"
/>
<input
type="password"
value={authPassword}
onChange={(e) => setAuthPassword(e.target.value)}
placeholder="Password"
className="tool-input w-full"
/>
</div>
)}
</div>
{/* Request Body */}
{['POST', 'PUT', 'PATCH'].includes(method) && (
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Request Body (JSON)
</label>
<div className="flex items-center gap-2">
<button
onClick={() => {
if (bodyViewMode === 'raw') {
// Switch to visual: parse JSON
try {
const data = JSON.parse(body || '{}');
setBodyStructuredData(data);
setBodyViewMode('visual');
} catch (e) {
alert('Invalid JSON. Please fix syntax errors first.');
}
} else {
// Switch to raw: stringify structured data
setBody(JSON.stringify(bodyStructuredData, null, 2));
setBodyViewMode('raw');
}
}}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
title={bodyViewMode === 'raw' ? 'Switch to visual editor' : 'Switch to raw JSON'}
>
{bodyViewMode === 'raw' ? (
<>
<Braces className="h-3 w-3" /> Visual Editor
</>
) : (
<>
<Code className="h-3 w-3" /> Raw JSON
</>
)}
</button>
</div>
</div>
{bodyViewMode === 'raw' ? (
<div className="border border-gray-300 dark:border-gray-600 rounded-md overflow-hidden">
<CodeMirrorEditor
value={body}
onChange={setBody}
language="json"
placeholder='{"key": "value"}'
height="150px"
/>
</div>
) : (
<div className="border border-gray-300 dark:border-gray-600 rounded-md p-3 bg-white dark:bg-gray-800 max-h-96 overflow-y-auto">
<StructuredEditor
initialData={bodyStructuredData}
onDataChange={(newData) => {
setBodyStructuredData(newData);
setBody(JSON.stringify(newData, null, 2));
}}
readOnly={false}
/>
</div>
)}
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
💡 Tip: Toggle between raw JSON and visual tree editor for easier editing
</p>
</div>
)}
{/* Save/Load Presets */}
<div className="border-t border-gray-300 dark:border-gray-600 pt-4">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300 block mb-2">
Request Presets
</label>
<div className="flex gap-2 mb-3">
<input
type="text"
value={presetName}
onChange={(e) => setPresetName(e.target.value)}
placeholder="Preset name"
className="tool-input flex-1 text-sm"
/>
<button
onClick={savePreset}
className="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm flex items-center gap-1"
>
<Save className="h-4 w-4" /> Save
</button>
</div>
{savedPresets.length > 0 && (
<div className="space-y-1">
{savedPresets.map((preset, index) => (
<div
key={index}
className="flex items-center justify-between p-2 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-600"
>
<button
onClick={() => loadPreset(preset)}
className="flex-1 text-left text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 flex items-center gap-2"
>
<FolderOpen className="h-4 w-4" />
<span className="font-medium">{preset.name}</span>
<span className="text-xs text-gray-500">({preset.method})</span>
</button>
<button
onClick={() => deletePreset(index)}
className="text-red-600 hover:text-red-700 dark:text-red-400"
>
<X className="h-4 w-4" />
</button>
</div>
))}
</div>
)}
</div>
</div>
) : (
<ProFeatureLock
featureName={featureInfo?.name || 'Advanced URL Fetch'}
featureDescription={featureInfo?.description || 'Unlock custom HTTP methods, headers, authentication, and request body configuration'}
onUpgrade={onUpgrade}
/>
)
)}
<p className="text-xs text-gray-500 dark:text-gray-400">
{showAdvanced
? 'Configure HTTP method, headers, authentication, and request body for API testing'
: 'Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.'
}
</p>
</div>
);
};
export default AdvancedURLFetch;

View File

@@ -1,10 +1,12 @@
import React, { useRef, useEffect, useState, useCallback } from 'react'; import React, { useRef, useEffect, useState, useCallback } from 'react';
import { EditorView } from '@codemirror/view'; import { EditorView, keymap } from '@codemirror/view';
import { EditorState } from '@codemirror/state'; import { EditorState } from '@codemirror/state';
import { basicSetup } from 'codemirror'; import { basicSetup } from 'codemirror';
import { json } from '@codemirror/lang-json'; import { json } from '@codemirror/lang-json';
import { sql } from '@codemirror/lang-sql'; import { sql } from '@codemirror/lang-sql';
import { markdown } from '@codemirror/lang-markdown';
import { oneDark } from '@codemirror/theme-one-dark'; import { oneDark } from '@codemirror/theme-one-dark';
import { indentWithTab } from '@codemirror/commands';
import { Maximize2, Minimize2 } from 'lucide-react'; import { Maximize2, Minimize2 } from 'lucide-react';
const CodeMirrorEditor = ({ const CodeMirrorEditor = ({
@@ -15,7 +17,8 @@ const CodeMirrorEditor = ({
language = 'json', language = 'json',
maxLines = 12, maxLines = 12,
showToggle = true, showToggle = true,
cardRef = null // Reference to the card header for scroll target cardRef = null, // Reference to the card header for scroll target
height = '350px' // Configurable height, default 350px
}) => { }) => {
const editorRef = useRef(null); const editorRef = useRef(null);
const viewRef = useRef(null); const viewRef = useRef(null);
@@ -53,33 +56,42 @@ const CodeMirrorEditor = ({
langExtension = [json()]; langExtension = [json()];
} else if (language === 'sql') { } else if (language === 'sql') {
langExtension = [sql()]; langExtension = [sql()];
} else if (language === 'markdown') {
langExtension = [markdown()];
} }
const extensions = [ const extensions = [
basicSetup, basicSetup,
...langExtension, ...langExtension,
// Enable line wrapping for single-line content // Enable Tab key to insert spaces (2 spaces for indentation)
...(isSingleLine ? [EditorView.lineWrapping] : []), keymap.of([indentWithTab]),
// Enable line wrapping for single-line content OR markdown
...(isSingleLine || language === 'markdown' ? [EditorView.lineWrapping] : []),
EditorView.theme({ EditorView.theme({
'&': { '&': {
fontSize: '14px', fontSize: '14px',
width: '100%', width: '100%',
maxWidth: '100%',
}, },
'.cm-content': { '.cm-content': {
padding: '12px', padding: '12px',
maxWidth: '100%',
}, },
'.cm-focused': { '.cm-focused': {
outline: 'none', outline: 'none',
}, },
'.cm-editor': { '.cm-editor': {
borderRadius: '6px', borderRadius: '6px',
maxWidth: '100%',
}, },
'.cm-scroller': { '.cm-scroller': {
overflowY: 'auto', overflowY: 'auto',
overflowX: 'auto',
height: '100%', height: '100%',
maxWidth: '100%',
}, },
'.cm-line': { '.cm-line': {
wordBreak: isSingleLine ? 'break-word' : 'normal', wordBreak: isSingleLine || language === 'markdown' ? 'break-word' : 'normal',
} }
}), }),
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
@@ -102,14 +114,22 @@ const CodeMirrorEditor = ({
viewRef.current = view; viewRef.current = view;
// Expose view on the DOM element for toolbar access
if (editorRef.current) {
const cmEditor = editorRef.current.querySelector('.cm-editor');
if (cmEditor) {
cmEditor.cmView = { view };
}
}
// Apply styles immediately after editor creation // Apply styles immediately after editor creation
setTimeout(() => { setTimeout(() => {
const editorElement = editorRef.current?.querySelector('.cm-editor'); const editorElement = editorRef.current?.querySelector('.cm-editor');
const scrollerElement = editorRef.current?.querySelector('.cm-scroller'); const scrollerElement = editorRef.current?.querySelector('.cm-scroller');
if (editorElement) { if (editorElement) {
editorElement.style.height = '350px'; editorElement.style.height = height;
editorElement.style.maxHeight = '350px'; editorElement.style.maxHeight = height;
} }
if (scrollerElement) { if (scrollerElement) {
@@ -128,7 +148,7 @@ const CodeMirrorEditor = ({
} }
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDark, isSingleLine]); // Recreate when theme or line count changes }, [isDark, isSingleLine, height, language]); // Recreate when theme, line count, height, or language changes
// Apply overflow and height styles // Apply overflow and height styles
const applyEditorStyles = useCallback(() => { const applyEditorStyles = useCallback(() => {
@@ -142,8 +162,8 @@ const CodeMirrorEditor = ({
editorElement.style.height = 'auto'; editorElement.style.height = 'auto';
editorElement.style.maxHeight = 'none'; editorElement.style.maxHeight = 'none';
} else { } else {
editorElement.style.height = '350px'; editorElement.style.height = height;
editorElement.style.maxHeight = '350px'; editorElement.style.maxHeight = height;
} }
} }
@@ -152,7 +172,7 @@ const CodeMirrorEditor = ({
scrollerElement.style.overflowY = isExpanded ? 'visible' : 'auto'; scrollerElement.style.overflowY = isExpanded ? 'visible' : 'auto';
scrollerElement.style.height = '100%'; scrollerElement.style.height = '100%';
} }
}, [isExpanded]); }, [isExpanded, height]);
// Apply styles on mount, expand/collapse, and content changes // Apply styles on mount, expand/collapse, and content changes
useEffect(() => { useEffect(() => {
@@ -182,7 +202,7 @@ const CodeMirrorEditor = ({
ref={editorRef} ref={editorRef}
className={`dewedev-code-mirror border border-gray-300 dark:border-gray-600 rounded-md overflow-hidden ${ className={`dewedev-code-mirror border border-gray-300 dark:border-gray-600 rounded-md overflow-hidden ${
isDark ? 'bg-gray-900' : 'bg-white' isDark ? 'bg-gray-900' : 'bg-white'
} ${isExpanded ? 'h-auto' : 'h-[350px]'}`} } ${isExpanded ? 'h-auto' : `h-[${height}]`}`}
/> />
{showToggle && ( {showToggle && (
<button <button

View File

@@ -57,7 +57,7 @@ const Layout = ({ children }) => {
{/* Header */} {/* Header */}
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0"> <header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className={isToolPage ? "px-4 sm:px-6 lg:px-8" : "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"}>
<div className="flex justify-between items-center h-16"> <div className="flex justify-between items-center h-16">
<button onClick={() => navigateWithGuard('/')} className="flex items-center group"> <button onClick={() => navigateWithGuard('/')} className="flex items-center group">
<div className="relative"> <div className="relative">
@@ -250,16 +250,16 @@ const Layout = ({ children }) => {
)} )}
{/* Main Content */} {/* Main Content */}
<div className="flex flex-1 pt-16"> <div className="flex flex-1 pt-16 min-w-0 w-full max-w-full overflow-x-hidden">
{/* Main Content Area */} {/* Main Content Area */}
<main className="flex-1 flex flex-col"> <main className="flex-1 flex flex-col min-w-0 w-full max-w-full">
{isToolPage && !isInvoicePreviewPage ? ( {isToolPage && !isInvoicePreviewPage ? (
<div className="block"> <div className="block">
<div className="hidden lg:block fixed top-16 left-0 z-[9999]"> <div className="hidden lg:block fixed top-16 left-0 z-[9999]">
<ToolSidebar navigateWithGuard={navigateWithGuard} /> <ToolSidebar navigateWithGuard={navigateWithGuard} />
</div> </div>
<div className="flex-1 flex flex-col min-h-0 pl-0 lg:pl-16"> <div className="flex-1 flex flex-col pl-0 lg:pl-16 min-w-0">
<div className="flex-1 p-4 sm:p-6 w-full min-w-0 overflow-auto"> <div className="p-4 sm:p-6 w-full min-w-0 max-w-full overflow-x-hidden">
{children} {children}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,66 @@
import React, { useEffect, useState } from 'react';
import { X } from 'lucide-react';
/**
* MobileAdBanner Component - Sticky bottom banner for mobile
* Visible only on mobile/tablet, hidden on desktop
* Includes close button for better UX
*/
const MobileAdBanner = ({ slot = 'REPLACE_WITH_MOBILE_SLOT' }) => {
const [visible, setVisible] = useState(true);
const [closed, setClosed] = useState(false);
useEffect(() => {
// Check if user previously closed the banner (session storage)
const wasClosed = sessionStorage.getItem('mobileAdClosed');
if (wasClosed === 'true') {
setClosed(true);
setVisible(false);
}
}, []);
useEffect(() => {
if (visible && !closed) {
try {
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
console.error('AdSense error:', e);
}
}
}, [visible, closed]);
const handleClose = () => {
setVisible(false);
setClosed(true);
sessionStorage.setItem('mobileAdClosed', 'true');
};
if (!visible || closed) return null;
return (
<div className="xl:hidden fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 shadow-lg border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleClose}
className="absolute top-1 right-1 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded-full shadow-sm"
aria-label="Close ad"
>
<X className="h-4 w-4" />
</button>
<div className="flex justify-center py-2">
<ins
className="adsbygoogle"
style={{
display: 'inline-block',
width: '320px',
height: '50px'
}}
data-ad-client="ca-pub-8644544686212757"
data-ad-slot={slot}
data-ad-format="fixed"
/>
</div>
</div>
);
};
export default MobileAdBanner;

123
src/components/ProBadge.js Normal file
View File

@@ -0,0 +1,123 @@
import React from 'react';
import { Crown, Lock } from 'lucide-react';
/**
* ProBadge Component
*
* Displays a PRO badge for premium features
* Can be used inline or as a button to trigger upgrade flow
*/
const ProBadge = ({
variant = 'badge', // 'badge' | 'button' | 'inline'
size = 'sm', // 'xs' | 'sm' | 'md' | 'lg'
onClick = null,
showIcon = true,
className = ''
}) => {
const sizeClasses = {
xs: 'text-xs px-1.5 py-0.5',
sm: 'text-xs px-2 py-1',
md: 'text-sm px-3 py-1.5',
lg: 'text-base px-4 py-2'
};
const iconSizes = {
xs: 'h-2.5 w-2.5',
sm: 'h-3 w-3',
md: 'h-4 w-4',
lg: 'h-5 w-5'
};
if (variant === 'inline') {
return (
<span className={`inline-flex items-center gap-1 text-amber-600 dark:text-amber-400 font-semibold ${className}`}>
{showIcon && <Crown className={iconSizes[size]} />}
<span className={sizeClasses[size]}>PRO</span>
</span>
);
}
if (variant === 'button') {
return (
<button
onClick={onClick}
className={`inline-flex items-center gap-1.5 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white font-semibold rounded-full transition-all transform hover:scale-105 ${sizeClasses[size]} ${className}`}
>
{showIcon && <Crown className={iconSizes[size]} />}
Upgrade to PRO
</button>
);
}
// Default badge variant
return (
<span className={`inline-flex items-center gap-1 bg-gradient-to-r from-amber-500 to-orange-500 text-white font-bold rounded-full ${sizeClasses[size]} ${className}`}>
{showIcon && <Crown className={iconSizes[size]} />}
PRO
</span>
);
};
/**
* ProFeatureLock Component
*
* Displays a locked feature message with upgrade prompt
*/
export const ProFeatureLock = ({
featureName,
featureDescription,
onUpgrade = null,
compact = false
}) => {
const handleUpgrade = () => {
if (onUpgrade) {
onUpgrade();
} else {
// Default: scroll to top and show upgrade info
window.scrollTo({ top: 0, behavior: 'smooth' });
alert('Upgrade to PRO to unlock this feature!\n\nPRO features will be available soon.');
}
};
if (compact) {
return (
<div className="flex items-center gap-2 p-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg">
<Lock className="h-4 w-4 text-amber-600 dark:text-amber-400 flex-shrink-0" />
<span className="text-sm text-amber-800 dark:text-amber-300 flex-1">
<ProBadge variant="inline" size="xs" showIcon={false} /> feature
</span>
<button
onClick={handleUpgrade}
className="text-xs text-amber-700 dark:text-amber-300 hover:underline font-medium whitespace-nowrap"
>
Upgrade
</button>
</div>
);
}
return (
<div className="p-4 bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 border-2 border-amber-200 dark:border-amber-700 rounded-lg">
<div className="flex items-start gap-3">
<div className="p-2 bg-amber-100 dark:bg-amber-800/50 rounded-lg">
<Lock className="h-5 w-5 text-amber-600 dark:text-amber-400" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-semibold text-gray-900 dark:text-gray-100">
{featureName}
</h4>
<ProBadge size="sm" />
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
{featureDescription}
</p>
<ProBadge variant="button" size="md" onClick={handleUpgrade} />
</div>
</div>
</div>
);
};
export default ProBadge;

View File

@@ -0,0 +1,96 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
/**
* Related Tools Component
* Shows related tools at the bottom of each tool page for internal linking
*/
const RELATED_TOOLS = {
'markdown-editor': [
{ name: 'Code Beautifier', path: '/beautifier', desc: 'Format and beautify your code' },
{ name: 'Text Length Checker', path: '/text-length', desc: 'Count words and characters' },
{ name: 'Diff Tool', path: '/diff', desc: 'Compare text differences' }
],
'object-editor': [
{ name: 'Table Editor', path: '/table-editor', desc: 'Edit JSON as table' },
{ name: 'Code Beautifier', path: '/beautifier', desc: 'Format & beautify code' },
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Write documentation' }
],
'table-editor': [
{ name: 'Object Editor', path: '/object-editor', desc: 'Edit JSON visually' },
{ name: 'Code Beautifier', path: '/beautifier', desc: 'Format & beautify code' },
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Create documentation' }
],
'invoice-editor': [
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Create documentation' },
{ name: 'Table Editor', path: '/table-editor', desc: 'Manage data tables' },
{ name: 'Object Editor', path: '/object-editor', desc: 'Edit JSON data' }
],
'beautifier': [
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Write documentation' },
{ name: 'Diff Tool', path: '/diff', desc: 'Compare code changes' },
{ name: 'Object Editor', path: '/object-editor', desc: 'Edit JSON visually' }
],
'diff': [
{ name: 'Beautifier', path: '/beautifier', desc: 'Format code first' },
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Document changes' },
{ name: 'Text Length', path: '/text-length', desc: 'Analyze text' }
],
'text-length': [
{ name: 'Markdown Editor', path: '/markdown-editor', desc: 'Write content' },
{ name: 'Diff Tool', path: '/diff', desc: 'Compare texts' },
{ name: 'Beautifier', path: '/beautifier', desc: 'Format code' }
],
'url': [
{ name: 'Base64 Encoder', path: '/base64', desc: 'Encode/decode data' },
{ name: 'Code Beautifier', path: '/beautifier', desc: 'Format code' },
{ name: 'Object Editor', path: '/object-editor', desc: 'Edit JSON data' }
],
'base64': [
{ name: 'URL Encoder', path: '/url', desc: 'Encode URLs' },
{ name: 'Object Editor', path: '/object-editor', desc: 'Edit JSON & PHP data' },
{ name: 'Code Beautifier', path: '/beautifier', desc: 'Format code' }
]
};
const RelatedTools = ({ toolId }) => {
const relatedTools = RELATED_TOOLS[toolId];
if (!relatedTools || relatedTools.length === 0) {
return null;
}
return (
<div className="mt-8 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-800 dark:to-gray-900 rounded-lg border border-blue-100 dark:border-gray-700 p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<span className="text-blue-600 dark:text-blue-400"></span>
You Might Also Like
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{relatedTools.map((tool) => (
<Link
key={tool.path}
to={tool.path}
className="group bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-500 hover:shadow-md transition-all duration-200"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{tool.name}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{tool.desc}
</p>
</div>
<ArrowRight className="h-5 w-5 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 group-hover:translate-x-1 transition-all flex-shrink-0 ml-2" />
</div>
</Link>
))}
</div>
</div>
);
};
export default RelatedTools;

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import TOOL_FAQS from '../data/faqs';
const SEO = ({ const SEO = ({
title, title,
@@ -7,7 +8,8 @@ const SEO = ({
keywords, keywords,
path = '/', path = '/',
type = 'website', type = 'website',
image = 'https://dewe.dev/og-image.png' image = 'https://dewe.dev/og-image.png',
toolId = null
}) => { }) => {
const siteUrl = 'https://dewe.dev'; const siteUrl = 'https://dewe.dev';
const fullUrl = `${siteUrl}${path}`; const fullUrl = `${siteUrl}${path}`;
@@ -17,6 +19,47 @@ const SEO = ({
const defaultKeywords = 'developer tools, json editor, csv converter, base64 encoder, url encoder, code beautifier, diff tool, web developer utilities, online tools'; const defaultKeywords = 'developer tools, json editor, csv converter, base64 encoder, url encoder, code beautifier, diff tool, web developer utilities, online tools';
const metaKeywords = keywords || defaultKeywords; const metaKeywords = keywords || defaultKeywords;
// Get FAQ data for this tool
const faqs = toolId && TOOL_FAQS[toolId] ? TOOL_FAQS[toolId] : null;
// Generate breadcrumb schema
const breadcrumbSchema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": siteUrl
}
]
};
// Add current page to breadcrumb if not homepage
if (path !== '/') {
breadcrumbSchema.itemListElement.push({
"@type": "ListItem",
"position": 2,
"name": title || "Page",
"item": fullUrl
});
}
// Generate FAQ schema if FAQs exist
const faqSchema = faqs ? {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": faqs.map(faq => ({
"@type": "Question",
"name": faq.question,
"acceptedAnswer": {
"@type": "Answer",
"text": faq.answer
}
}))
} : null;
return ( return (
<Helmet> <Helmet>
{/* Basic Meta Tags */} {/* Basic Meta Tags */}
@@ -46,7 +89,7 @@ const SEO = ({
<meta name="author" content="Developer Tools" /> <meta name="author" content="Developer Tools" />
<meta name="language" content="English" /> <meta name="language" content="English" />
{/* JSON-LD Structured Data */} {/* JSON-LD Structured Data - WebApplication */}
<script type="application/ld+json"> <script type="application/ld+json">
{JSON.stringify({ {JSON.stringify({
"@context": "https://schema.org", "@context": "https://schema.org",
@@ -68,6 +111,18 @@ const SEO = ({
} }
})} })}
</script> </script>
{/* JSON-LD Structured Data - Breadcrumb */}
<script type="application/ld+json">
{JSON.stringify(breadcrumbSchema)}
</script>
{/* JSON-LD Structured Data - FAQ (if available) */}
{faqSchema && (
<script type="application/ld+json">
{JSON.stringify(faqSchema)}
</script>
)}
</Helmet> </Helmet>
); );
}; };

View File

@@ -8,7 +8,8 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
const isInternalUpdate = useRef(false); const isInternalUpdate = useRef(false);
const [nestedEditModal, setNestedEditModal] = useState(null); // { path, value, type: 'json' | 'serialized' } const [nestedEditModal, setNestedEditModal] = useState(null); // { path, value, type: 'json' | 'serialized' }
const [nestedData, setNestedData] = useState(null); const [nestedData, setNestedData] = useState(null);
const [editMode, setEditMode] = useState(false); // Internal edit mode state // Start in edit mode if readOnly is false
const [editMode, setEditMode] = useState(readOnlyProp === false);
// Use internal editMode if readOnlyProp is not explicitly set, otherwise use prop // Use internal editMode if readOnlyProp is not explicitly set, otherwise use prop
const readOnly = readOnlyProp !== false ? readOnlyProp : !editMode; const readOnly = readOnlyProp !== false ? readOnlyProp : !editMode;

View File

@@ -1,29 +1,45 @@
import React from 'react'; import React from 'react';
import AdColumn from './AdColumn';
import MobileAdBanner from './MobileAdBanner';
const ToolLayout = ({ title, description, children, icon: Icon }) => { const ToolLayout = ({ title, description, children, icon: Icon }) => {
return ( return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full" style={{ maxWidth: 'min(80rem, calc(100vw - 2rem))' }}> <>
{/* Header */} <div className="w-full max-w-full px-0 sm:px-6 lg:px-8 flex gap-5 min-w-0">
<div className="mb-6 sm:mb-8"> {/* Main Content */}
<div className="flex items-center space-x-2 sm:space-x-3 mb-2"> <div className="flex-1 min-w-0 max-w-full">
{Icon && <Icon className="h-6 w-6 sm:h-8 sm:w-8 text-primary-600 flex-shrink-0" />} {/* Header */}
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white truncate"> <div className="mb-6 sm:mb-8 min-w-0">
{title} <div className="flex items-center space-x-2 sm:space-x-3 mb-2 min-w-0">
</h1> {Icon && <Icon className="h-6 w-6 sm:h-8 sm:w-8 text-primary-600 flex-shrink-0" />}
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white truncate min-w-0">
{title}
</h1>
</div>
{description && (
<p className="text-gray-600 dark:text-gray-300 text-base sm:text-lg break-words">
{description}
</p>
)}
</div>
{/* Tool Content */}
<div className="space-y-4 sm:space-y-6 w-full max-w-full min-w-0">
{children}
</div>
</div> </div>
{description && ( {/* Desktop Ad Column - Hidden on mobile */}
<p className="text-gray-600 dark:text-gray-300 text-base sm:text-lg"> <AdColumn />
{description}
</p>
)}
</div> </div>
{/* Tool Content */} {/* Mobile Ad Banner - Hidden on desktop */}
<div className="space-y-4 sm:space-y-6 w-full"> <MobileAdBanner />
{children}
</div> {/* Add padding to bottom on mobile to prevent content overlap with sticky ad */}
</div> <div className="xl:hidden h-16" />
</>
); );
}; };

81
src/config/features.js Normal file
View File

@@ -0,0 +1,81 @@
/**
* Feature Toggle System
*
* Controls which features are available in FREE vs PRO versions
* Currently static, will be dynamic from database in the future
*/
// User tier - will be fetched from database/auth in the future
export const USER_TIER = {
FREE: 'free',
PRO: 'pro'
};
// Current user tier (static for now, will be dynamic)
// TODO: Replace with actual user tier from authentication/database
export const getCurrentUserTier = () => {
// For development/testing, you can change this
const staticTier = USER_TIER.PRO; // Change to USER_TIER.PRO to test pro features
// In the future, this will be:
// return getUserFromAuth()?.tier || USER_TIER.FREE;
return staticTier;
};
// Feature flags
export const FEATURES = {
// Advanced URL Fetch with headers, methods, body, auth
ADVANCED_URL_FETCH: {
name: 'Advanced URL Fetch',
description: 'Custom HTTP methods, headers, authentication, and request body',
tier: USER_TIER.PRO,
enabled: (userTier) => userTier === USER_TIER.PRO
},
// Future pro features can be added here
BULK_OPERATIONS: {
name: 'Bulk Operations',
description: 'Process multiple files or operations at once',
tier: USER_TIER.PRO,
enabled: (userTier) => userTier === USER_TIER.PRO
},
EXPORT_TEMPLATES: {
name: 'Export Templates',
description: 'Save and reuse custom export templates',
tier: USER_TIER.PRO,
enabled: (userTier) => userTier === USER_TIER.PRO
},
CLOUD_SYNC: {
name: 'Cloud Sync',
description: 'Sync your data and settings across devices',
tier: USER_TIER.PRO,
enabled: (userTier) => userTier === USER_TIER.PRO
}
};
// Helper function to check if a feature is enabled
export const isFeatureEnabled = (featureName) => {
const feature = FEATURES[featureName];
if (!feature) return false;
const userTier = getCurrentUserTier();
return feature.enabled(userTier);
};
// Helper function to get user tier
export const getUserTier = () => {
return getCurrentUserTier();
};
// Helper function to check if user is pro
export const isProUser = () => {
return getCurrentUserTier() === USER_TIER.PRO;
};
// Helper function to get feature info
export const getFeatureInfo = (featureName) => {
return FEATURES[featureName] || null;
};

View File

@@ -63,6 +63,14 @@ export const TOOLS = [
tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor'], tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor'],
category: 'editor' category: 'editor'
}, },
{
path: '/markdown-editor',
name: 'Markdown Editor',
icon: FileText,
description: 'Write and preview markdown with live rendering, syntax highlighting, and export options',
tags: ['Markdown', 'Editor', 'Preview', 'Export', 'GFM'],
category: 'editor'
},
{ {
path: '/invoice-editor', path: '/invoice-editor',
name: 'Invoice Editor', name: 'Invoice Editor',

100
src/data/faqs.js Normal file
View File

@@ -0,0 +1,100 @@
/**
* FAQ Data for SEO Schema Markup
* Used to generate FAQPage structured data for better search visibility
*/
export const TOOL_FAQS = {
'markdown-editor': [
{
question: "Is this markdown editor free to use?",
answer: "Yes, completely free with no limits. All processing happens in your browser with no data upload or storage. No account needed."
},
{
question: "Does it support GitHub Flavored Markdown?",
answer: "Yes, full GitHub Flavored Markdown (GFM) support including tables, task lists, strikethrough, and syntax highlighting for code blocks."
},
{
question: "Can I export my markdown to PDF?",
answer: "Yes, you can export your markdown to PDF, HTML, or plain text with one click. The PDF export maintains proper formatting and styling."
},
{
question: "Is my data safe and private?",
answer: "100% safe. All processing happens locally in your browser. We never upload, store, or transmit your data to any server. Your content stays on your device."
},
{
question: "Does it work offline?",
answer: "Yes, once loaded, the markdown editor works completely offline. No internet connection required for editing or exporting."
}
],
'object-editor': [
{
question: "What is the Object Editor used for?",
answer: "The Object Editor is a visual tool for editing JSON objects, nested data structures, arrays, and complex data. Perfect for developers working with APIs, configuration files, or database records."
},
{
question: "Can I import JSON from a URL?",
answer: "Yes, you can fetch JSON data directly from any URL or API endpoint. The editor will parse and display it in an easy-to-edit visual format."
},
{
question: "Does it validate JSON syntax?",
answer: "Yes, real-time JSON validation with detailed error messages. The editor highlights syntax errors and helps you fix them quickly."
},
{
question: "Is my data stored anywhere?",
answer: "No, all data processing happens in your browser. We never upload, store, or transmit your data. 100% privacy-first and secure."
},
{
question: "Can I edit nested objects and arrays?",
answer: "Yes, full support for nested objects, arrays, and complex data structures. Add, edit, delete properties at any level with visual controls."
}
],
'table-editor': [
{
question: "What file formats can I import?",
answer: "Import CSV, TSV, JSON, Excel (XLSX/XLS), SQL dumps, and paste from spreadsheets like Google Sheets or Excel. Automatic format detection included."
},
{
question: "Can I export to Excel format?",
answer: "Yes, export to Excel (XLSX), CSV, TSV, JSON, Markdown tables, HTML tables, and SQL INSERT statements. Choose the format you need."
},
{
question: "Is there a row or column limit?",
answer: "No hard limit. The editor efficiently handles thousands of rows and columns in your browser without performance issues."
},
{
question: "Can I edit JSON data in table cells?",
answer: "Yes, the editor detects JSON/serialized data in cells and lets you edit it visually in the Object Editor. Seamless integration between tools."
},
{
question: "Does it work with SQL databases?",
answer: "Yes, import SQL dumps from phpMyAdmin or other tools, edit the data visually, and export back to SQL INSERT statements for database import."
}
],
'invoice-editor': [
{
question: "Is the invoice editor free?",
answer: "Yes, completely free with no limits. Create unlimited invoices with professional templates. No account or subscription required."
},
{
question: "What invoice templates are available?",
answer: "Multiple professional templates: Standard, Modern, Minimal, and Classic. Each template is customizable with your branding and colors."
},
{
question: "Can I save my invoices?",
answer: "Yes, save invoices as JSON files to your device. Load them anytime to edit or create new invoices. All data stays on your device."
},
{
question: "Can I export to PDF?",
answer: "Yes, export invoices to PDF with professional formatting. Perfect for sending to clients or printing. One-click export included."
},
{
question: "Does it calculate taxes and totals automatically?",
answer: "Yes, automatic calculation of subtotals, taxes, discounts, and final totals. Supports multiple tax rates and discount types (percentage or fixed amount)."
}
]
};
export default TOOL_FAQS;

View File

@@ -8,6 +8,8 @@
html { html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow-x: hidden; overflow-x: hidden;
width: 100%;
max-width: 100vw;
} }
body { body {
@@ -16,6 +18,13 @@
max-width: 100vw; max-width: 100vw;
} }
#root {
overflow-x: hidden;
width: 100%;
max-width: 100vw;
min-width: 0;
}
code, pre { code, pre {
font-family: 'JetBrains Mono', Monaco, 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', monospace; font-family: 'JetBrains Mono', Monaco, 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', monospace;
} }
@@ -38,6 +47,14 @@
@apply px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md font-medium transition-colors duration-200; @apply px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md font-medium transition-colors duration-200;
} }
.tool-button-primary {
@apply flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md font-medium transition-colors duration-200;
}
.toolbar-btn {
@apply p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 font-medium text-sm;
}
.copy-button { .copy-button {
@apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200; @apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200;
} }

View File

@@ -1,315 +0,0 @@
import React, { useState } from 'react';
import { RefreshCw, Upload } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
const CsvJsonTool = () => {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [mode, setMode] = useState('csv-to-json'); // 'csv-to-json' or 'json-to-csv'
const [delimiter, setDelimiter] = useState(',');
const [hasHeaders, setHasHeaders] = useState(true);
const csvToJson = () => {
try {
const lines = input.trim().split('\n');
if (lines.length === 0) {
setOutput('Error: No data to convert');
return;
}
const headers = hasHeaders ? lines[0].split(delimiter).map(h => h.trim()) : null;
const dataLines = hasHeaders ? lines.slice(1) : lines;
const result = dataLines.map((line, index) => {
const values = line.split(delimiter).map(v => v.trim());
if (hasHeaders && headers) {
const obj = {};
headers.forEach((header, i) => {
obj[header] = values[i] || '';
});
return obj;
} else {
return values;
}
});
setOutput(JSON.stringify(result, null, 2));
} catch (err) {
setOutput(`Error: ${err.message}`);
}
};
const jsonToCsv = () => {
try {
const data = JSON.parse(input);
let csv = '';
if (Array.isArray(data)) {
// Handle array of objects (original functionality)
if (data.length === 0) {
setOutput('Error: Empty array');
return;
}
// Get headers from first object
const headers = Object.keys(data[0]);
// Add headers if enabled
if (hasHeaders) {
csv += headers.join(delimiter) + '\n';
}
// Add data rows
data.forEach(row => {
const values = headers.map(header => {
const value = row[header] || '';
// Escape values containing delimiter or quotes
if (typeof value === 'string' && (value.includes(delimiter) || value.includes('"') || value.includes('\n'))) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
});
csv += values.join(delimiter) + '\n';
});
} else if (typeof data === 'object' && data !== null) {
// Handle single object as key-value pairs
// Add headers if enabled
if (hasHeaders) {
csv += `Key${delimiter}Value\n`;
}
// Add key-value rows
Object.entries(data).forEach(([key, value]) => {
// Format the key
let formattedKey = key;
if (typeof key === 'string' && (key.includes(delimiter) || key.includes('"') || key.includes('\n'))) {
formattedKey = `"${key.replace(/"/g, '""')}"`;
}
// Format the value
let formattedValue = '';
if (value === null) {
formattedValue = 'null';
} else if (value === undefined) {
formattedValue = 'undefined';
} else if (typeof value === 'object') {
// Convert objects/arrays to JSON string
formattedValue = JSON.stringify(value);
} else {
formattedValue = String(value);
}
// Escape value if needed
if (typeof formattedValue === 'string' && (formattedValue.includes(delimiter) || formattedValue.includes('"') || formattedValue.includes('\n'))) {
formattedValue = `"${formattedValue.replace(/"/g, '""')}"`;
}
csv += `${formattedKey}${delimiter}${formattedValue}\n`;
});
} else {
setOutput('Error: JSON must be an object or an array of objects');
return;
}
setOutput(csv.trim());
} catch (err) {
setOutput(`Error: ${err.message}`);
}
};
const handleProcess = () => {
if (mode === 'csv-to-json') {
csvToJson();
} else {
jsonToCsv();
}
};
const handleFileUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
setInput(e.target.result);
};
reader.readAsText(file);
}
};
const clearAll = () => {
setInput('');
setOutput('');
};
const loadSample = () => {
if (mode === 'csv-to-json') {
setInput(`name,age,email,city
John Doe,30,john@example.com,New York
Jane Smith,25,jane@example.com,Los Angeles
Bob Johnson,35,bob@example.com,Chicago`);
} else {
setInput(`[
{
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"city": "New York"
},
{
"name": "Jane Smith",
"age": 25,
"email": "jane@example.com",
"city": "Los Angeles"
}
]`);
}
};
return (
<ToolLayout
title="CSV ↔ JSON Converter"
description="Convert between CSV and JSON formats with custom delimiters"
icon={RefreshCw}
>
{/* Mode Toggle */}
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
<button
onClick={() => setMode('csv-to-json')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'csv-to-json'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
CSV JSON
</button>
<button
onClick={() => setMode('json-to-csv')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'json-to-csv'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
JSON CSV
</button>
</div>
{/* Options */}
<div className="flex flex-wrap items-center gap-4 mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center space-x-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Delimiter:
</label>
<select
value={delimiter}
onChange={(e) => setDelimiter(e.target.value)}
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm"
>
<option value=",">Comma (,)</option>
<option value=";">Semicolon (;)</option>
<option value="\t">Tab</option>
<option value="|">Pipe (|)</option>
</select>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="hasHeaders"
checked={hasHeaders}
onChange={(e) => setHasHeaders(e.target.checked)}
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
<label htmlFor="hasHeaders" className="text-sm font-medium text-gray-700 dark:text-gray-300">
First row contains headers
</label>
</div>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-3 mb-6">
<button onClick={handleProcess} className="tool-button">
{mode === 'csv-to-json' ? 'Convert to JSON' : 'Convert to CSV'}
</button>
<label className="tool-button-secondary cursor-pointer flex items-center space-x-2">
<Upload className="h-4 w-4" />
<span>Upload File</span>
<input
type="file"
onChange={handleFileUpload}
className="hidden"
accept=".csv,.json,.txt"
/>
</label>
<button onClick={loadSample} className="tool-button-secondary">
Load Sample
</button>
<button onClick={clearAll} className="tool-button-secondary">
Clear All
</button>
</div>
{/* Input/Output Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Input */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'csv-to-json' ? 'CSV Input' : 'JSON Input'}
</label>
<div className="relative">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={
mode === 'csv-to-json'
? 'Paste your CSV data here...'
: 'Paste your JSON array here...'
}
className="tool-input h-96"
/>
</div>
</div>
{/* Output */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'csv-to-json' ? 'JSON Output' : 'CSV Output'}
</label>
<div className="relative">
<textarea
value={output}
readOnly
placeholder={
mode === 'csv-to-json'
? 'JSON output will appear here...'
: 'CSV output will appear here...'
}
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
/>
{output && <CopyButton text={output} />}
</div>
</div>
</div>
{/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-2">Usage Tips</h4>
<ul className="text-blue-700 dark:text-blue-300 text-sm space-y-1">
<li> CSV to JSON converts each row to an object with column headers as keys</li>
<li> JSON to CSV requires an array of objects with consistent properties</li>
<li> Choose the appropriate delimiter for your CSV format</li>
<li> Toggle "First row contains headers" based on your data structure</li>
</ul>
</div>
</ToolLayout>
);
};
export default CsvJsonTool;

View File

@@ -6,6 +6,8 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import ToolLayout from '../components/ToolLayout'; import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor'; import CodeMirrorEditor from '../components/CodeMirrorEditor';
import SEO from '../components/SEO';
import RelatedTools from '../components/RelatedTools';
const InvoiceEditor = () => { const InvoiceEditor = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -745,39 +747,48 @@ const InvoiceEditor = () => {
}; };
return ( return (
<ToolLayout <>
title="Invoice Editor" <SEO
description="Create, edit, and export professional invoices with PDF generation" title="Free Invoice Generator - Professional Invoice Templates"
> description="✓ Free invoice generator ✓ Professional templates ✓ PDF export ✓ Auto-calculate totals ✓ No signup. Create invoices now!"
<div className="space-y-4 sm:space-y-6 w-full"> keywords="invoice generator, invoice maker, invoice template, free invoice, invoice editor, pdf invoice, professional invoice, online invoice, invoice creator"
{/* Input Section */} path="/invoice-editor"
<div className="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"> toolId="invoice-editor"
<div className="border-b border-gray-200 dark:border-gray-700"> />
<nav className="flex space-x-2 sm:space-x-8 px-4 sm:px-6 overflow-x-auto" aria-label="Tabs"> <ToolLayout
{[ title="Invoice Editor"
{ id: 'create', name: 'Create New', icon: Plus }, description="Create, edit, and export professional invoices with PDF generation"
{ id: 'url', name: 'URL', icon: Globe }, icon={FileText}
{ id: 'paste', name: 'Paste', icon: FileText }, >
{ id: 'open', name: 'Open', icon: Upload } <div className="space-y-4 sm:space-y-6 w-full">
].map((tab) => { {/* Input Section */}
const Icon = tab.icon; <div className="w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
return ( <div className="border-b border-gray-200 dark:border-gray-700">
<button <nav className="flex space-x-2 sm:space-x-8 px-4 sm:px-6 overflow-x-auto" aria-label="Tabs">
key={tab.id} {[
onClick={() => handleTabChange(tab.id)} { id: 'create', name: 'Create New', icon: Plus },
className={`${ { id: 'url', name: 'URL', icon: Globe },
activeTab === tab.id { id: 'paste', name: 'Paste', icon: FileText },
? 'border-blue-500 text-blue-600 dark:text-blue-400' { id: 'open', name: 'Open', icon: Upload }
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300' ].map((tab) => {
} whitespace-nowrap py-4 px-1 sm:px-2 border-b-2 font-medium text-sm flex items-center gap-1 sm:gap-2 transition-colors min-w-0 flex-shrink-0`} const Icon = tab.icon;
> return (
<Icon className="h-4 w-4" /> <button
{tab.name} key={tab.id}
</button> onClick={() => handleTabChange(tab.id)}
); className={`${
})} activeTab === tab.id
</nav> ? 'border-blue-500 text-blue-600 dark:text-blue-400'
</div> : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
} whitespace-nowrap py-4 px-1 sm:px-2 border-b-2 font-medium text-sm flex items-center gap-1 sm:gap-2 transition-colors min-w-0 flex-shrink-0`}
>
<Icon className="h-4 w-4" />
{tab.name}
</button>
);
})}
</nav>
</div>
{/* Tab Content */} {/* Tab Content */}
{(activeTab !== 'create' || !createNewCompleted) && ( {(activeTab !== 'create' || !createNewCompleted) && (
@@ -2122,7 +2133,11 @@ const InvoiceEditor = () => {
/> />
</div> </div>
{/* Related Tools */}
<RelatedTools toolId="invoice-editor" />
</ToolLayout> </ToolLayout>
</>
); );
}; };

View File

@@ -1,254 +0,0 @@
import React, { useState } from 'react';
import { Code, AlertCircle, CheckCircle, Edit3 } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
import StructuredEditor from '../components/StructuredEditor';
import CodeEditor from '../components/CodeEditor';
const JsonTool = () => {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [error, setError] = useState('');
const [isValid, setIsValid] = useState(null);
const [editorMode, setEditorMode] = useState('text'); // 'text' or 'visual'
const [structuredData, setStructuredData] = useState({});
const formatJson = () => {
try {
const parsed = JSON.parse(input);
const formatted = JSON.stringify(parsed, null, 2);
setOutput(formatted);
setError('');
setIsValid(true);
} catch (err) {
setError(`Invalid JSON: ${err.message}`);
setOutput('');
setIsValid(false);
}
};
const minifyJson = () => {
try {
const parsed = JSON.parse(input);
const minified = JSON.stringify(parsed);
setOutput(minified);
setError('');
setIsValid(true);
} catch (err) {
setError(`Invalid JSON: ${err.message}`);
setOutput('');
setIsValid(false);
}
};
const validateJson = () => {
try {
JSON.parse(input);
setError('');
setIsValid(true);
setOutput('✅ Valid JSON');
} catch (err) {
setError(`Invalid JSON: ${err.message}`);
setIsValid(false);
setOutput('');
}
};
const clearAll = () => {
setInput('');
setOutput('');
setError('');
setIsValid(null);
};
const handleStructuredDataChange = (newData) => {
setStructuredData(newData);
setInput(JSON.stringify(newData, null, 2));
setError('');
setIsValid(true);
};
const switchToVisualEditor = () => {
try {
const parsed = input ? JSON.parse(input) : {};
setStructuredData(parsed);
setEditorMode('visual');
setError('');
setIsValid(true);
} catch (err) {
setError(`Cannot switch to visual editor: ${err.message}`);
setIsValid(false);
}
};
const switchToTextEditor = () => {
setEditorMode('text');
};
const loadSample = () => {
const sample = {
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"address": {
"street": "123 Main St",
"city": "New York",
"zipCode": "10001"
},
"hobbies": ["reading", "coding", "traveling"],
"isActive": true
};
setInput(JSON.stringify(sample, null, 2));
setStructuredData(sample);
};
return (
<ToolLayout
title="JSON Encoder/Decoder"
description="Format, validate, and minify JSON data with syntax highlighting"
icon={Code}
>
{/* Editor Mode Toggle */}
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
<button
onClick={switchToTextEditor}
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
editorMode === 'text'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
<Code className="h-4 w-4" />
<span>Text Editor</span>
</button>
<button
onClick={switchToVisualEditor}
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
editorMode === 'visual'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
<Edit3 className="h-4 w-4" />
<span>Visual Editor</span>
</button>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-3 mb-6">
<button onClick={formatJson} className="tool-button">
Format JSON
</button>
<button onClick={minifyJson} className="tool-button">
Minify JSON
</button>
<button onClick={validateJson} className="tool-button">
Validate JSON
</button>
<button onClick={loadSample} className="tool-button-secondary">
Load Sample
</button>
<button onClick={clearAll} className="tool-button-secondary">
Clear All
</button>
</div>
{/* Status Indicator */}
{isValid !== null && (
<div className={`flex items-center space-x-2 p-3 rounded-md mb-4 ${
isValid
? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300'
: 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300'
}`}>
{isValid ? (
<CheckCircle className="h-5 w-5" />
) : (
<AlertCircle className="h-5 w-5" />
)}
<span className="font-medium">
{isValid ? 'Valid JSON' : 'Invalid JSON'}
</span>
</div>
)}
{/* Error Display */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4 mb-4">
<div className="flex items-start space-x-2">
<AlertCircle className="h-5 w-5 text-red-500 mt-0.5" />
<div>
<h4 className="text-red-800 dark:text-red-200 font-medium">Error</h4>
<p className="text-red-700 dark:text-red-300 text-sm mt-1">{error}</p>
</div>
</div>
</div>
)}
{/* Input/Output Grid */}
<div className={`grid gap-6 ${
editorMode === 'visual'
? 'grid-cols-1'
: 'grid-cols-1 lg:grid-cols-2'
}`}>
{/* Input */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{editorMode === 'text' ? 'Input JSON' : 'Visual JSON Editor'}
</label>
<div className="relative">
{editorMode === 'text' ? (
<CodeEditor
value={input}
onChange={(value) => setInput(value)}
language="json"
placeholder="Paste your JSON here..."
height="400px"
className="w-full"
/>
) : (
<div className="min-h-96">
<StructuredEditor
initialData={structuredData}
onDataChange={handleStructuredDataChange}
/>
</div>
)}
</div>
</div>
{/* Output */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Output
</label>
<div className="relative">
<CodeEditor
value={output}
language="json"
readOnly={true}
placeholder="Formatted JSON will appear here..."
height="400px"
className="w-full"
/>
<div className="absolute top-2 right-2">
<CopyButton text={output} />
</div>
</div>
</div>
</div>
{/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-2">Usage Tips</h4>
<ul className="text-blue-700 dark:text-blue-300 text-sm space-y-1">
<li> Use "Format JSON" to beautify and indent your JSON</li>
<li> Use "Minify JSON" to compress JSON by removing whitespace</li>
<li> Use "Validate JSON" to check if your JSON syntax is correct</li>
<li> Click the copy button to copy the output to your clipboard</li>
</ul>
</div>
</ToolLayout>
);
};
export default JsonTool;

2277
src/pages/MarkdownEditor.js Normal file

File diff suppressed because it is too large Load Diff

93
src/pages/NotFound.js Normal file
View File

@@ -0,0 +1,93 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Home, Search, FileText, Edit3, Table, FileCode } from 'lucide-react';
import SEO from '../components/SEO';
const NotFound = () => {
const popularTools = [
{ name: 'Markdown Editor', path: '/markdown-editor', icon: FileText, desc: 'Write & preview markdown' },
{ name: 'Object Editor', path: '/object-editor', icon: Edit3, desc: 'Visual JSON editor' },
{ name: 'Table Editor', path: '/table-editor', icon: Table, desc: 'Edit CSV, JSON, Excel' },
{ name: 'Code Beautifier', path: '/beautifier', icon: FileCode, desc: 'Format & beautify code' }
];
return (
<>
<SEO
title="Page Not Found - 404"
description="The page you're looking for doesn't exist. Explore our free developer tools including JSON formatter, markdown editor, and more."
path="/404"
/>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex items-center justify-center px-4">
<div className="max-w-2xl w-full text-center">
{/* 404 Number */}
<div className="mb-8">
<h1 className="text-9xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent animate-pulse">
404
</h1>
<div className="mt-4 flex items-center justify-center gap-2">
<Search className="h-6 w-6 text-gray-400" />
<p className="text-2xl font-semibold text-gray-700 dark:text-gray-300">
Page Not Found
</p>
</div>
</div>
{/* Message */}
<p className="text-lg text-gray-600 dark:text-gray-400 mb-12">
Oops! The page you're looking for doesn't exist. It might have been moved or deleted.
</p>
{/* Popular Tools */}
<div className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
Try These Popular Tools Instead:
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{popularTools.map((tool) => {
const Icon = tool.icon;
return (
<Link
key={tool.path}
to={tool.path}
className="group bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-500 hover:shadow-lg transition-all duration-200"
>
<div className="flex items-start gap-4">
<div className="p-3 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-lg group-hover:scale-110 transition-transform">
<Icon className="h-6 w-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1 text-left">
<h3 className="font-semibold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{tool.name}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
{tool.desc}
</p>
</div>
</div>
</Link>
);
})}
</div>
</div>
{/* Home Button */}
<Link
to="/"
className="inline-flex items-center gap-2 px-8 py-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition-all duration-200 transform hover:scale-105"
>
<Home className="h-5 w-5" />
Go to Homepage
</Link>
{/* Search Suggestion */}
<p className="mt-8 text-sm text-gray-500 dark:text-gray-400">
Or use the search bar at the top to find what you need
</p>
</div>
</div>
</>
);
};
export default NotFound;

View File

@@ -1,10 +1,14 @@
import React, { useState, useRef, useEffect, useCallback } from 'react'; import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Plus, Upload, FileText, Globe, Edit3, Download, Workflow, Table, Braces, Code, AlertTriangle, ChevronUp, ChevronDown } from 'lucide-react'; import { Plus, Upload, FileText, Globe, Edit3, Download, Workflow, Table, Braces, Code, AlertTriangle, ChevronUp, ChevronDown } from 'lucide-react';
import ToolLayout from '../components/ToolLayout'; import ToolLayout from '../components/ToolLayout';
import StructuredEditor from '../components/StructuredEditor'; import StructuredEditor from '../components/StructuredEditor';
import MindmapView from '../components/MindmapView'; import MindmapView from '../components/MindmapView';
import PostmanTable from '../components/PostmanTable'; import PostmanTable from '../components/PostmanTable';
import CodeMirrorEditor from '../components/CodeMirrorEditor'; import CodeMirrorEditor from '../components/CodeMirrorEditor';
import AdvancedURLFetch from '../components/AdvancedURLFetch';
import SEO from '../components/SEO';
import RelatedTools from '../components/RelatedTools';
import { extractContentFromUrl, CONTENT_TYPE_INFO } from '../utils/contentExtractor';
const ObjectEditor = () => { const ObjectEditor = () => {
const exportCardRef = useRef(null); const exportCardRef = useRef(null);
@@ -29,6 +33,7 @@ const ObjectEditor = () => {
}); });
const [fetchUrl, setFetchUrl] = useState(''); const [fetchUrl, setFetchUrl] = useState('');
const [fetching, setFetching] = useState(false); const [fetching, setFetching] = useState(false);
// const [showAdvanced, setShowAdvanced] = useState(false); // Hidden for now
const [createNewCompleted, setCreateNewCompleted] = useState(false); const [createNewCompleted, setCreateNewCompleted] = useState(false);
const [showInputChangeModal, setShowInputChangeModal] = useState(false); const [showInputChangeModal, setShowInputChangeModal] = useState(false);
const [pendingTabChange, setPendingTabChange] = useState(null); const [pendingTabChange, setPendingTabChange] = useState(null);
@@ -603,43 +608,124 @@ const ObjectEditor = () => {
reader.readAsText(file); reader.readAsText(file);
}; };
// Fetch data from URL // Fetch data from URL with advanced content extraction
const handleFetchData = async () => { const handleFetchData = async (advancedOptions = null) => {
if (!fetchUrl.trim()) { console.log('🚀 handleFetchData called with URL:', fetchUrl);
console.log('🔧 Advanced options:', advancedOptions);
const urlToFetch = advancedOptions?.url || fetchUrl.trim();
if (!urlToFetch) {
setError('Please enter a valid URL'); setError('Please enter a valid URL');
return; return;
} }
setFetching(true); setFetching(true);
setError(''); setError('');
console.log('✅ Starting fetch process...');
try { try {
// Add protocol if missing // Add protocol if missing
let url = fetchUrl.trim(); let url = urlToFetch;
if (!url.startsWith('http://') && !url.startsWith('https://')) { if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url; url = 'https://' + url;
} }
const response = await fetch(url); // Determine if this is an advanced request (has custom options)
const isAdvancedRequest = advancedOptions && (
if (!response.ok) { advancedOptions.method !== 'GET' ||
throw new Error(`HTTP ${response.status}: ${response.statusText}`); Object.keys(advancedOptions.headers || {}).length > 0 ||
advancedOptions.body
);
console.log('🎯 Is advanced request:', isAdvancedRequest);
// Build fetch options for advanced mode
const fetchOptions = advancedOptions ? {
method: advancedOptions.method || 'GET',
headers: advancedOptions.headers || {},
body: advancedOptions.body || undefined
} : {};
console.log('📡 Fetch options:', fetchOptions);
// Try direct fetch first (for APIs)
try {
const response = await fetch(url, fetchOptions);
if (response.ok) {
const contentType = response.headers.get('content-type');
const text = await response.text();
// Try to parse as JSON
if (contentType?.includes('application/json') || text.trim().startsWith('{') || text.trim().startsWith('[')) {
try {
const data = JSON.parse(text);
setStructuredData(data);
generateOutputs(data);
setInputText(JSON.stringify(data, null, 2));
setInputFormat('JSON');
setInputValid(true);
setCreateNewCompleted(true);
// Set URL data summary
setUrlDataSummary({
format: 'JSON',
size: text.length,
properties: Object.keys(data).length,
url: url,
contentType: isAdvancedRequest ? `API Response (${advancedOptions.method})` : 'API Response'
});
setFetching(false);
return;
} catch (e) {
// Not valid JSON, continue to content extraction only if simple GET
if (isAdvancedRequest) {
throw new Error('Response is not valid JSON. Content-Type: ' + (contentType || 'unknown'));
}
}
} else if (isAdvancedRequest) {
// For advanced requests with non-JSON response, show error
throw new Error('Response is not JSON. Content-Type: ' + (contentType || 'unknown'));
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (e) {
// If advanced request fails, don't try content extraction
if (isAdvancedRequest) {
throw e;
}
// CORS error or network error, will try with content extractor for simple GET
console.log('Direct fetch failed, trying content extractor:', e.message);
} }
const contentType = response.headers.get('content-type'); // Use advanced content extractor ONLY for simple GET requests to HTML pages
const text = await response.text(); const result = await extractContentFromUrl(url);
let data;
if (!contentType || !contentType.includes('application/json')) { // Create structured data from extracted content
// Try to parse as JSON anyway, some APIs don't set correct content-type const data = {
try { url: result.url,
data = JSON.parse(text); title: result.title,
} catch { contentType: result.contentType,
throw new Error('Response is not valid JSON. Content-Type: ' + (contentType || 'unknown')); contentQuality: CONTENT_TYPE_INFO[result.contentType]?.label || result.contentType,
} metadata: {
} else { description: result.description,
data = JSON.parse(text); author: result.author,
} publishDate: result.publishDate,
wordCount: result.metrics.articleWordCount,
readingTime: Math.ceil(result.metrics.articleWordCount / 200) + ' min'
},
structure: result.structure,
content: {
articleText: result.articleText,
allText: result.allText
},
metrics: result.metrics
};
setStructuredData(data); setStructuredData(data);
generateOutputs(data); generateOutputs(data);
@@ -648,13 +734,18 @@ const ObjectEditor = () => {
setInputValid(true); setInputValid(true);
setCreateNewCompleted(true); setCreateNewCompleted(true);
// Set URL data summary // Set URL data summary with content type info
const contentInfo = CONTENT_TYPE_INFO[result.contentType];
setUrlDataSummary({ setUrlDataSummary({
format: 'JSON', format: 'Extracted Content',
size: text.length, size: JSON.stringify(data).length,
properties: Object.keys(data).length, properties: Object.keys(data).length,
url: url url: url,
contentType: result.contentType,
contentTypeLabel: contentInfo?.label,
contentTypeEmoji: contentInfo?.emoji
}); });
} catch (err) { } catch (err) {
console.error('Fetch error:', err); console.error('Fetch error:', err);
setError(`Failed to fetch data: ${err.message}`); setError(`Failed to fetch data: ${err.message}`);
@@ -670,11 +761,19 @@ const ObjectEditor = () => {
}, [structuredData, generateOutputs]); }, [structuredData, generateOutputs]);
return ( return (
<ToolLayout <>
title="Object Editor" <SEO
description="Visual editor for JSON and PHP serialized objects with format conversion" title="Online JSON & Object Editor - Visual Data Editor"
icon={Edit3} description="✓ Edit JSON visually ✓ Fetch from URLs ✓ Nested objects & arrays ✓ Format conversion ✓ Privacy-first. Try it free - no data upload!"
> keywords="json editor, object editor, json formatter, json validator, visual json editor, online json tool, api testing, json viewer, nested json editor"
path="/object-editor"
toolId="object-editor"
/>
<ToolLayout
title="Object Editor"
description="Visual editor for JSON and PHP serialized objects with format conversion"
icon={Edit3}
>
{/* Input Section with Tabs */} {/* Input Section with Tabs */}
<div className="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-4 sm:mb-6"> <div className="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-4 sm:mb-6">
{/* Tabs */} {/* Tabs */}
@@ -812,46 +911,43 @@ const ObjectEditor = () => {
{/* URL Tab Content */} {/* URL Tab Content */}
{activeTab === 'url' && ( {activeTab === 'url' && (
urlDataSummary ? ( <div className="p-4">
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg"> {urlDataSummary && (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<span className="text-sm text-green-700 dark:text-green-300 break-words"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.properties} {urlDataSummary.properties === 1 ? 'property' : 'properties'}) <div className="flex flex-col gap-1">
</span> <span className="text-sm text-green-700 dark:text-green-300 break-words">
<button ✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.properties} {urlDataSummary.properties === 1 ? 'property' : 'properties'})
onClick={() => setUrlDataSummary(null)} </span>
className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap" {urlDataSummary.contentTypeLabel && (
> <span className="text-xs text-gray-600 dark:text-gray-400">
Fetch New URL ▼ {urlDataSummary.contentTypeEmoji} {urlDataSummary.contentTypeLabel}
</button> </span>
</div> )}
</div> </div>
) : ( <button
<div className="space-y-3"> onClick={() => setUrlDataSummary(null)}
<div className="flex gap-2"> className="text-sm text-blue-600 dark:text-blue-400 hover:underline whitespace-nowrap"
<div className="relative flex-1"> >
<input Fetch New URL ▼
type="url" </button>
value={fetchUrl}
onChange={(e) => setFetchUrl(e.target.value)}
placeholder="https://api.telegram.org/bot<token>/getMe"
className="tool-input w-full"
onKeyPress={(e) => e.key === 'Enter' && handleFetchData()}
/>
</div> </div>
<button
onClick={handleFetchData}
disabled={fetching || !fetchUrl.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
>
{fetching ? 'Fetching...' : 'Fetch Data'}
</button>
</div> </div>
<p className="text-xs text-gray-500 dark:text-gray-400"> )}
Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.
</p> {/* Keep component mounted but hidden to preserve state */}
<div style={{ display: urlDataSummary ? 'none' : 'block' }}>
<AdvancedURLFetch
url={fetchUrl}
onUrlChange={setFetchUrl}
onFetch={handleFetchData}
fetching={fetching}
showAdvanced={false}
onToggleAdvanced={() => {}}
onUpgrade={() => {}}
/>
</div> </div>
) </div>
)} )}
{/* Paste Tab Content */} {/* Paste Tab Content */}
@@ -1282,7 +1378,11 @@ const ObjectEditor = () => {
</div> </div>
)} )}
</div> </div>
{/* Related Tools */}
<RelatedTools toolId="object-editor" />
</ToolLayout> </ToolLayout>
</>
); );
}; };

View File

@@ -1,547 +0,0 @@
import React, { useState } from 'react';
import { Database, Edit3 } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
import StructuredEditor from '../components/StructuredEditor';
const SerializeTool = () => {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [mode, setMode] = useState('serialize'); // 'serialize' or 'unserialize'
const [error, setError] = useState('');
const [editorMode, setEditorMode] = useState('text'); // 'text' or 'visual'
const [structuredData, setStructuredData] = useState({});
// Simple PHP serialize implementation for common data types
const phpSerialize = (data) => {
if (data === null) return 'N;';
if (typeof data === 'boolean') return data ? 'b:1;' : 'b:0;';
if (typeof data === 'number') {
return Number.isInteger(data) ? `i:${data};` : `d:${data};`;
}
if (typeof data === 'string') {
// Escape quotes and backslashes in the string first
const escapedData = data.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
// PHP serialize requires UTF-8 byte length of the ESCAPED string
const byteLength = new TextEncoder().encode(escapedData).length;
return `s:${byteLength}:"${escapedData}";`;
}
if (Array.isArray(data)) {
let result = `a:${data.length}:{`;
data.forEach((item, index) => {
result += phpSerialize(index) + phpSerialize(item);
});
result += '}';
return result;
}
if (typeof data === 'object') {
const keys = Object.keys(data);
let result = `a:${keys.length}:{`;
keys.forEach(key => {
result += phpSerialize(key) + phpSerialize(data[key]);
});
result += '}';
return result;
}
return 'N;';
};
// Simple PHP unserialize implementation
const phpUnserialize = (str) => {
let index = 0;
const parseValue = () => {
if (index >= str.length) {
throw new Error('Unexpected end of string');
}
const type = str[index];
// Handle NULL case (no colon after N)
if (type === 'N') {
index += 2; // Skip 'N;'
return null;
}
// For all other types, expect colon after type
if (str[index + 1] !== ':') {
throw new Error(`Expected ':' after type '${type}' at position ${index + 1}`);
}
index += 2; // Skip type and ':'
switch (type) {
case 'b':
const boolVal = str[index] === '1';
index += 2; // Skip value and ';'
return boolVal;
case 'i':
let intStr = '';
while (index < str.length && str[index] !== ';') {
intStr += str[index++];
}
if (index >= str.length) {
throw new Error('Unexpected end of string while parsing integer');
}
index++; // Skip ';'
return parseInt(intStr);
case 'd':
let floatStr = '';
while (index < str.length && str[index] !== ';') {
floatStr += str[index++];
}
if (index >= str.length) {
throw new Error('Unexpected end of string while parsing float');
}
index++; // Skip ';'
return parseFloat(floatStr);
case 's':
let lenStr = '';
while (index < str.length && str[index] !== ':') {
lenStr += str[index++];
}
if (index >= str.length) {
throw new Error('Unexpected end of string while parsing string length');
}
index++; // Skip ':'
// Expect opening quote
if (str[index] !== '"') {
throw new Error(`Expected '"' at position ${index}`);
}
index++; // Skip opening '"'
const byteLength = parseInt(lenStr);
if (isNaN(byteLength) || byteLength < 0) {
throw new Error(`Invalid string length: ${lenStr}`);
}
// Handle empty strings
if (byteLength === 0) {
// Expect closing quote and semicolon immediately
if (index + 1 >= str.length || str[index] !== '"' || str[index + 1] !== ';') {
throw new Error(`Expected '";' after empty string at position ${index}`);
}
index += 2; // Skip closing '";'
return '';
}
// Find the actual end of the string by looking for the closing quote-semicolon pattern
const startIndex = index;
let endQuotePos = -1;
// Look for the pattern '";' starting from the current position
for (let i = startIndex; i < str.length - 1; i++) {
if (str[i] === '"' && str[i + 1] === ';') {
endQuotePos = i;
break;
}
}
if (endQuotePos === -1) {
throw new Error(`Could not find closing '";' for string starting at position ${startIndex}`);
}
// Extract the actual string content
const stringVal = str.substring(startIndex, endQuotePos);
const actualByteLength = new TextEncoder().encode(stringVal).length;
// Move index to after the closing '";'
index = endQuotePos + 2;
// Warn about byte length mismatch but continue parsing
if (actualByteLength !== byteLength) {
console.warn(`Warning: String byte length mismatch - declared ${byteLength}, actual ${actualByteLength}`);
}
return stringVal;
case 'a':
let arrayLenStr = '';
while (index < str.length && str[index] !== ':') {
arrayLenStr += str[index++];
}
if (index >= str.length) {
throw new Error('Unexpected end of string while parsing array length');
}
index++; // Skip ':'
// Expect opening brace
if (str[index] !== '{') {
throw new Error(`Expected '{' at position ${index}`);
}
index++; // Skip '{'
const arrayLength = parseInt(arrayLenStr);
if (isNaN(arrayLength) || arrayLength < 0) {
throw new Error(`Invalid array length: ${arrayLenStr}`);
}
const result = {};
let isArray = true;
for (let i = 0; i < arrayLength; i++) {
const key = parseValue();
const value = parseValue();
result[key] = value;
// Check if this looks like a sequential array
if (typeof key !== 'number' || key !== i) {
isArray = false;
}
}
// Expect closing brace
if (index >= str.length || str[index] !== '}') {
throw new Error(`Expected '}' at position ${index}`);
}
index++; // Skip '}'
// Convert to array if all keys are sequential integers starting from 0
if (isArray && arrayLength > 0) {
const arr = [];
for (let i = 0; i < arrayLength; i++) {
arr[i] = result[i];
}
return arr;
}
return result;
default:
throw new Error(`Unknown type: '${type}' at position ${index - 2}`);
}
};
try {
const result = parseValue();
// Check if there's unexpected trailing data
if (index < str.length) {
console.warn(`Warning: Trailing data after parsing: "${str.substring(index)}"`);
}
return result;
} catch (error) {
throw new Error(`Parse error at position ${index}: ${error.message}`);
}
};
const handleSerialize = () => {
try {
const data = JSON.parse(input);
const serialized = phpSerialize(data);
setOutput(serialized);
} catch (err) {
setOutput(`Error: ${err.message}`);
}
};
const handleUnserialize = () => {
try {
const unserialized = phpUnserialize(input);
setOutput(JSON.stringify(unserialized, null, 2));
} catch (err) {
setOutput(`Error: ${err.message}`);
}
};
const handleProcess = () => {
if (mode === 'serialize') {
handleSerialize();
} else {
handleUnserialize();
}
};
// Editor mode switching functions
const switchToTextEditor = () => {
if (editorMode === 'visual') {
try {
const jsonString = JSON.stringify(structuredData, null, 2);
setInput(jsonString);
} catch (err) {
setError('Error converting structured data to JSON');
}
}
setEditorMode('text');
};
const switchToVisualEditor = () => {
if (editorMode === 'text' && input.trim()) {
try {
const parsed = JSON.parse(input);
setStructuredData(parsed);
setError('');
} catch (err) {
setError('Invalid JSON format. Please fix the JSON before switching to visual editor.');
return;
}
}
setEditorMode('visual');
};
const handleStructuredDataChange = (newData) => {
setStructuredData(newData);
try {
const jsonString = JSON.stringify(newData, null, 2);
setInput(jsonString);
setError('');
} catch (err) {
setError('Error updating JSON from structured data');
}
};
// Function to open output in visual editor
const openInVisualEditor = () => {
try {
// Parse the output to validate it's JSON
const parsedData = JSON.parse(output);
// Switch to serialize mode
setMode('serialize');
// Set the input with the output content
setInput(output);
// Set structured data for visual editor
setStructuredData(parsedData);
// Switch to visual editor mode
setEditorMode('visual');
// Clear any errors
setError('');
} catch (err) {
setError('Cannot open in visual editor: Invalid JSON format');
}
};
// Check if output contains valid JSON
const isValidJsonOutput = () => {
if (!output || output.startsWith('Error:')) return false;
try {
JSON.parse(output);
return true;
} catch {
return false;
}
};
const clearAll = () => {
setInput('');
setOutput('');
};
const loadSample = () => {
if (mode === 'serialize') {
setInput(`{
"name": "John Doe",
"age": 30,
"active": true,
"scores": [85, 92, 78],
"address": {
"street": "123 Main St",
"city": "New York"
}
}`);
} else {
setInput('a:4:{s:4:"name";s:8:"John Doe";s:3:"age";i:30;s:6:"active";b:1;s:6:"scores";a:3:{i:0;i:85;i:1;i:92;i:2;i:78;}}');
}
};
return (
<ToolLayout
title="Serialize Encoder/Decoder"
description="Encode and decode serialized data (PHP serialize format)"
icon={Database}
>
{/* Mode Toggle */}
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
<button
onClick={() => setMode('serialize')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'serialize'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
Serialize
</button>
<button
onClick={() => setMode('unserialize')}
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'unserialize'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
Unserialize
</button>
</div>
{/* Editor Mode Toggle - only show in serialize mode */}
{mode === 'serialize' && (
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 mb-6 w-fit">
<button
onClick={switchToTextEditor}
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
editorMode === 'text'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
<Database className="h-4 w-4" />
<span>Text Editor</span>
</button>
<button
onClick={switchToVisualEditor}
className={`flex items-center space-x-2 px-4 py-2 rounded-md font-medium transition-colors ${
editorMode === 'visual'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
}`}
>
<Edit3 className="h-4 w-4" />
<span>Visual Editor</span>
</button>
</div>
)}
{/* Controls */}
<div className="flex flex-wrap gap-3 mb-6">
<button onClick={handleProcess} className="tool-button">
{mode === 'serialize' ? 'Serialize Data' : 'Unserialize Data'}
</button>
<button onClick={loadSample} className="tool-button-secondary">
Load Sample
</button>
<button onClick={clearAll} className="tool-button-secondary">
Clear All
</button>
</div>
{/* Input/Output Grid */}
<div className={`grid gap-6 ${
mode === 'serialize' && editorMode === 'visual'
? 'grid-cols-1'
: 'grid-cols-1 lg:grid-cols-2'
}`}>
{/* Input */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'serialize'
? (editorMode === 'text' ? 'JSON to Serialize' : 'Visual Data Editor')
: 'Serialized Data to Decode'
}
</label>
<div className="relative">
{mode === 'serialize' && editorMode === 'visual' ? (
<div className="min-h-96">
<StructuredEditor
initialData={structuredData}
onDataChange={handleStructuredDataChange}
/>
</div>
) : (
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={
mode === 'serialize'
? 'Enter JSON data to serialize...'
: 'Enter serialized data to decode...'
}
className="tool-input h-96"
/>
)}
</div>
{error && (
<p className="text-sm text-red-600 dark:text-red-400 mt-2">
{error}
</p>
)}
</div>
{/* Output */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'serialize' ? 'Serialized Output' : 'JSON Output'}
</label>
{mode === 'unserialize' && isValidJsonOutput() && (
<button
onClick={openInVisualEditor}
className="flex items-center space-x-1 px-3 py-1 text-xs font-medium text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-md hover:bg-primary-100 dark:hover:bg-primary-900/30 transition-colors"
>
<Edit3 className="h-3 w-3" />
<span>View in Visual Editor</span>
</button>
)}
</div>
<div className="relative">
<textarea
value={output}
readOnly
placeholder={
mode === 'serialize'
? 'Serialized data will appear here...'
: 'Decoded JSON will appear here...'
}
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
/>
{output && <CopyButton text={output} />}
</div>
</div>
</div>
{/* Serialize Format Reference */}
<div className="bg-gray-50 dark:bg-gray-800 rounded-md p-4 mt-6">
<h4 className="text-gray-800 dark:text-gray-200 font-medium mb-3">PHP Serialize Format Reference</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600 dark:text-gray-400">String:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">s:length:"value";</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Integer:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">i:value;</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Boolean:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">b:0; or b:1;</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Null:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">N;</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Array:</span>
<span className="ml-2 font-mono">a:length:&#123;...&#125;</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Float:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">d:value;</span>
</div>
</div>
</div>
{/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-2">Usage Tips</h4>
<ul className="text-blue-700 dark:text-blue-300 text-sm space-y-1">
<li> PHP serialize format is commonly used for storing complex data structures</li>
<li> Input JSON data to serialize it into PHP format</li>
<li> Paste serialized data to convert it back to readable JSON</li>
<li> Supports strings, integers, floats, booleans, arrays, and objects</li>
</ul>
</div>
</ToolLayout>
);
};
export default SerializeTool;

View File

@@ -3,6 +3,8 @@ import { Plus, Upload, FileText, Globe, Download, X, Table, Trash2, Database, Br
import ToolLayout from '../components/ToolLayout'; import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor'; import CodeMirrorEditor from '../components/CodeMirrorEditor';
import StructuredEditor from "../components/StructuredEditor"; import StructuredEditor from "../components/StructuredEditor";
import SEO from '../components/SEO';
import RelatedTools from '../components/RelatedTools';
import Papa from "papaparse"; import Papa from "papaparse";
const TableEditor = () => { const TableEditor = () => {
@@ -1838,11 +1840,19 @@ const TableEditor = () => {
return ( return (
<ToolLayout <>
title="Table Editor" <SEO
description="Import, edit, and export tabular data from various sources" title="Excel-Like Table Editor - Import CSV, JSON, Excel Online"
icon={Table} description="✓ Import CSV, JSON, Excel ✓ Visual editing ✓ Export to multiple formats ✓ Sort & filter ✓ No installation. Edit tables online now!"
> keywords="table editor, csv editor, excel online, json to csv, csv to json, data editor, spreadsheet editor, online table, sql editor, database editor"
path="/table-editor"
toolId="table-editor"
/>
<ToolLayout
title="Table Editor"
description="Import, edit, and export tabular data from various sources"
icon={Table}
>
{/* Input Section with Tabs */} {/* Input Section with Tabs */}
<div className="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"> <div className="w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Tabs */} {/* Tabs */}
@@ -2344,7 +2354,7 @@ const TableEditor = () => {
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-[-1px] z-10"> <thead className="bg-gray-50 dark:bg-gray-700 sticky top-[-1px] z-10">
<tr> <tr>
<th <th
className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border-r border-gray-200 dark:border-gray-600 ${ className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 tracking-wider border-r border-gray-200 dark:border-gray-600 ${
frozenColumns > 0 frozenColumns > 0
? "sticky left-0 z-20 bg-blue-50 dark:!bg-blue-900" ? "sticky left-0 z-20 bg-blue-50 dark:!bg-blue-900"
: "" : ""
@@ -3261,7 +3271,11 @@ const TableEditor = () => {
</div> </div>
)} )}
</div> </div>
{/* Related Tools */}
<RelatedTools toolId="table-editor" />
</ToolLayout> </ToolLayout>
</>
); );
}; };

View File

@@ -0,0 +1,380 @@
/* GitHub-style Markdown Preview Styling */
.markdown-preview {
color: #24292f;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
word-break: break-word;
}
/* Ensure all child elements respect container width */
.markdown-preview * {
max-width: 100%;
box-sizing: border-box;
}
.dark .markdown-preview {
color: #c9d1d9;
}
.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3,
.markdown-preview h4,
.markdown-preview h5,
.markdown-preview h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-preview h1 {
font-size: 2em;
border-bottom: 1px solid #d0d7de;
padding-bottom: 0.3em;
}
.dark .markdown-preview h1 {
border-bottom-color: #21262d;
}
.markdown-preview h2 {
font-size: 1.5em;
border-bottom: 1px solid #d0d7de;
padding-bottom: 0.3em;
}
.dark .markdown-preview h2 {
border-bottom-color: #21262d;
}
.markdown-preview h3 {
font-size: 1.25em;
}
.markdown-preview h4 {
font-size: 1em;
}
.markdown-preview h5 {
font-size: 0.875em;
}
.markdown-preview h6 {
font-size: 0.85em;
color: #57606a;
}
.dark .markdown-preview h6 {
color: #8b949e;
}
.markdown-preview p {
margin-top: 0;
margin-bottom: 16px;
}
/* Inline code - with background */
.markdown-preview code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(175, 184, 193, 0.2);
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}
.dark .markdown-preview code {
background-color: rgba(110, 118, 129, 0.4);
}
/* Code block wrapper with header */
.markdown-preview .code-block-wrapper {
margin-bottom: 16px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #d0d7de;
background-color: #f6f8fa;
}
.dark .markdown-preview .code-block-wrapper {
border-color: #30363d;
background-color: #0d1117;
}
/* Code block header */
.markdown-preview .code-block-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 10px;
background-color: #f6f8fa;
border-bottom: 1px solid #d0d7de;
font-size: 12px;
}
.dark .markdown-preview .code-block-header {
background-color: #161b22;
border-bottom-color: #30363d;
}
/* Language label */
.markdown-preview .code-block-language {
font-weight: 600;
color: #57606a;
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.5px;
}
.dark .markdown-preview .code-block-language {
color: #8b949e;
}
/* Copy button */
.markdown-preview .code-block-copy {
padding: 2px 6px;
background-color: transparent;
border: 1px solid #d0d7de;
border-radius: 6px;
color: #24292f;
font-size: 10px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
.markdown-preview .code-block-copy:hover {
background-color: #f3f4f6;
border-color: #1f2328;
}
.dark .markdown-preview .code-block-copy {
color: #c9d1d9;
border-color: #30363d;
}
.dark .markdown-preview .code-block-copy:hover {
background-color: #21262d;
border-color: #8b949e;
}
/* Code blocks - with background */
.markdown-preview .code-block-wrapper pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #0d1117;
margin: 0;
border-radius: 0;
}
/* Legacy pre blocks (without wrapper) */
.markdown-preview pre:not(.code-block-wrapper pre) {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #afb8c133;
border-radius: 6px;
margin-bottom: 16px;
}
.dark .markdown-preview pre:not(.code-block-wrapper pre) {
background-color: rgba(110, 118, 129, 0.4);
}
/* Code inside pre blocks - NO background (transparent) */
.markdown-preview pre code {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent !important;
border: 0;
border-radius: 0;
}
/* Preserve highlight.js syntax highlighting colors */
.markdown-preview pre code.hljs {
background: transparent !important;
padding: 0 !important;
}
.markdown-preview table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: 100%;
max-width: 100%;
overflow-x: auto;
margin-bottom: 16px;
}
.markdown-preview table tr {
background-color: #ffffff;
border-top: 1px solid #d0d7de;
}
.dark .markdown-preview table tr {
background-color: #0d1117;
border-top-color: #21262d;
}
.markdown-preview table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.dark .markdown-preview table tr:nth-child(2n) {
background-color: #161b22;
}
.markdown-preview table th,
.markdown-preview table td {
padding: 6px 13px;
border: 1px solid #d0d7de;
}
.dark .markdown-preview table th,
.dark .markdown-preview table td {
border-color: #21262d;
}
.markdown-preview table th {
font-weight: 600;
background-color: #f6f8fa;
}
.dark .markdown-preview table th {
background-color: #161b22;
}
.markdown-preview blockquote {
padding: 0 1em;
color: #57606a;
border-left: 0.25em solid #d0d7de;
margin: 0 0 16px 0;
}
.dark .markdown-preview blockquote {
color: #8b949e;
border-left-color: #3b434b;
}
.markdown-preview ul,
.markdown-preview ol {
padding-left: 2em;
margin-top: 0;
margin-bottom: 16px;
}
/* Nested lists */
.markdown-preview ul ul,
.markdown-preview ul ol,
.markdown-preview ol ul,
.markdown-preview ol ol {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
/* List items */
.markdown-preview li {
margin-bottom: 0.25em;
line-height: 1.6;
}
.markdown-preview li + li {
margin-top: 0.25em;
}
/* Better bullet points */
.markdown-preview ul > li {
list-style-type: disc;
}
.markdown-preview ul ul > li {
list-style-type: circle;
}
.markdown-preview ul ul ul > li {
list-style-type: square;
}
/* Ordered list styling */
.markdown-preview ol > li {
list-style-type: decimal;
}
.markdown-preview ol ol > li {
list-style-type: lower-alpha;
}
.markdown-preview ol ol ol > li {
list-style-type: lower-roman;
}
/* List item content spacing */
.markdown-preview li > p {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.markdown-preview li > p:first-child {
margin-top: 0;
}
.markdown-preview li > p:last-child {
margin-bottom: 0;
}
.markdown-preview hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #d0d7de;
border: 0;
}
.dark .markdown-preview hr {
background-color: #21262d;
}
.markdown-preview a {
color: #0969da;
text-decoration: none;
}
.dark .markdown-preview a {
color: #58a6ff;
}
.markdown-preview a:hover {
text-decoration: underline;
}
.markdown-preview strong {
font-weight: 600;
}
.markdown-preview em {
font-style: italic;
}
.markdown-preview u {
text-decoration: underline;
}
.markdown-preview img {
max-width: 100%;
height: auto;
border-radius: 6px;
margin: 16px 0;
}