Compare commits

...

25 Commits

Author SHA1 Message Date
dwindown
7ba289be5c feat: add ads.txt for AdSense verification 2025-10-23 00:09:54 +07:00
dwindown
d083129bd4 chore: update package-lock.json for serve dependency 2025-10-22 15:40:00 +07:00
dwindown
7d0a3e2a63 fix: add serve for production deployment 2025-10-22 15:36:32 +07:00
dwindown
fb9c944366 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
2025-10-22 15:20:22 +07:00
dwindown
08d345eaeb fix: use build:no-snap for deployment to avoid puppeteer/chrome dependency 2025-10-15 22:55:07 +07:00
dwindown
4158c15bff docs: simplify release notes for user clarity 2025-10-15 22:44:49 +07:00
dwindown
df0fb5d22a feat: Object Editor Preview Mode & Mobile Optimizations
Major Enhancements:
- Added Preview/Edit mode toggle to StructuredEditor component
  * Preview mode: Read-only view with full text visibility
  * Edit mode: Full editing capabilities with all controls
  * Toggle positioned below title, responsive on mobile
  * Works in both main ObjectEditor view and nested modals

- Clickable nested data in Preview mode
  * JSON/serialized values are blue and clickable
  * Opens modal directly without switching to Edit mode
  * Hover effects and tooltips for better UX
  * No longer need edit mode just to explore structure

Mobile Responsiveness Improvements:
- Fixed all data load notices in ObjectEditor (URL, Paste, Open tabs)
- Fixed all data load notices in TableEditor (URL, Paste, Open tabs)
- Notices now stack vertically on mobile with proper spacing
- Added break-words for long text, whitespace-nowrap for buttons
- Dark mode colors added for better visibility

Table Editor Fixes:
- Fixed sticky header showing row underneath (top-[-1px])
- Made Export section header mobile responsive
- Updated object modal footer layout:
  * Format info and properties combined on single line
  * Buttons moved to separate row below
  * Changed 'Apply Changes' to 'Save Changes' for consistency

StructuredEditor Improvements:
- Moved overflow-x handling from ObjectEditor to StructuredEditor
- Now works consistently in main view and nested modals
- Long strings scroll horizontally everywhere
- 'Add Property' button hidden in Preview mode
- Improved chevron colors for dark mode visibility

Technical Changes:
- StructuredEditor now manages its own editMode state
- readOnly prop can still be passed from parent if needed
- Proper conditional rendering for all UI elements
- Consistent mobile-first responsive design patterns
2025-10-15 22:40:57 +07:00
dwindown
f6c19e855d fix: improve SEO with pre-rendering and dynamic meta tags
Critical SEO improvements to fix Google Search Console indexing:

## Sitemap Updates:
- Added missing Invoice Editor and What's New pages
- Updated all lastmod dates to 2025-10-15
- Increased editor tools priority to 0.9
- Added organizational comments
- Fixed /whats-new route (was /release-notes)

## Pre-rendering Implementation:
- Added react-snap for static HTML generation
- Configured to pre-render all tool pages
- Solves React SPA indexing issue
- Crawlers now see full HTML content

## Dynamic Meta Tags:
- Added react-helmet-async for SEO management
- Created reusable SEO component with:
  - Dynamic titles and descriptions
  - Open Graph tags (Facebook)
  - Twitter Card tags
  - JSON-LD structured data
  - Canonical URLs
- Wrapped App with HelmetProvider
- Added SEO to Home page

## Route Fixes:
- Added /whats-new route (primary)
- Kept /release-notes as fallback
- Consistent routing across app

## Documentation:
- Created comprehensive SEO_FIX_GUIDE.md
- Step-by-step Google Search Console instructions
- Troubleshooting guide
- Timeline expectations
- Testing procedures

These changes will dramatically improve Google indexing and search visibility.
2025-10-15 10:01:48 +07:00
dwindown
f60c1d16c8 feat: comprehensive editor UX refinement with collapsible sections and data loss prevention
Major improvements to Object Editor, Table Editor, and Invoice Editor:

## UX Enhancements:
- Made export sections collapsible across all editors to reduce page height
- Added comprehensive, collapsible usage tips with eye-catching design
- Implemented consistent input method patterns (file auto-load, inline URL buttons)
- Paste sections now collapse after successful parsing with data summaries

## Data Loss Prevention:
- Added confirmation modals when switching input methods with existing data
- Amber-themed warning design with specific data summaries
- Suggests saving before proceeding with destructive actions
- Prevents accidental data loss across all editor tools

## Consistency Improvements:
- Standardized file input styling with 'tool-input' class
- URL fetch buttons now inline (not below input) across all editors
- Parse buttons positioned consistently on bottom-right
- Auto-load behavior for file inputs matching across editors

## Bug Fixes:
- Fixed Table Editor cell text overflow with proper truncation
- Fixed Object Editor file input to auto-load content
- Removed unnecessary parse buttons and checkboxes
- Fixed Invoice Editor URL form layout

## Documentation:
- Created EDITOR_TOOL_GUIDE.md with comprehensive patterns
- Created EDITOR_CHECKLIST.md for quick reference
- Created PROJECT_ROADMAP.md with future plans
- Created TODO.md with detailed task lists
- Documented data loss prevention patterns
- Added code examples and best practices

All editors now follow consistent UX patterns with improved user experience and data protection.
2025-10-15 00:12:54 +07:00
dwindown
14a07a6cba Fix ESLint warning in CodeMirrorEditor for CI build
- Properly positioned eslint-disable comment to suppress exhaustive-deps warning
- Prevents CI build failure due to warnings being treated as errors
- Build now compiles successfully in production environment
2025-09-28 23:40:14 +07:00
dwindown
68db19e076 UI consistency & code quality improvements
- Standardized InvoiceEditor CreateNew tab styling to match ObjectEditor design
- Fixed CodeMirror focus issues during editing by removing problematic dependencies
- Removed copy button from mindmap view for cleaner interface
- Resolved ESLint warnings in PostmanTreeTable.js with useCallback optimization
- Enhanced PDF generation with dynamic style swapping for better print output
- Updated commits.json with latest changes
2025-09-28 23:30:44 +07:00
dwindown
78570f04f0 feat: Enhanced release notes system, fixed invoice installments, and improved logo integration
- Updated release notes to use new JSON structure with individual commit timestamps
- Removed hash display from release notes for cleaner UI
- Fixed automatic recalculation of percentage-based installments in Invoice Editor and Preview
- Integrated custom logo.svg in header and footer with cleaner styling
- Moved all data files to /public/data/ for better organization
- Cleaned up unused release data files and improved file structure
2025-09-28 17:14:54 +07:00
dwindown
9993614073 feat: Dynamic What's New with Gitea API integration
 Fixed all ESLint warnings in analytics.js
 Created comprehensive releaseNotesAPI.js with multiple source support:
  - Static JSON fallback
  - Custom API endpoint support

 Updated ReleaseNotes component to use live Gitea API:
  - Uses environment variables for configuration
  - Graceful fallback to static data if API fails
  - Enhanced commit message parsing

 Build successful with no errors or warnings
 What's New feature now dynamically loads from your Git commits
2025-09-28 00:41:48 +07:00
dwindown
04db088ff9 feat: Invoice Editor improvements and code cleanup
Major Invoice Editor updates:
-  Fixed tripled scrollbar issue by removing unnecessary overflow classes
-  Implemented dynamic currency system with JSON data loading
-  Fixed F4 PDF generation error with proper paper size handling
-  Added proper padding to Total section matching table headers
-  Removed print functionality (users can print from PDF download)
-  Streamlined preview toolbar: Back, Size selector, Download PDF
-  Fixed all ESLint warnings and errors
-  Removed console.log statements across codebase for cleaner production
-  Added border-top to Total section for better visual consistency
-  Improved print CSS and removed JSX warnings

Additional improvements:
- Added currencies.json to public folder for proper HTTP access
- Enhanced MinimalTemplate with better spacing and layout
- Clean build with no warnings or errors
- Updated release notes with new features
2025-09-28 00:09:06 +07:00
dwindown
b2850ea145 fix: Remove unused variables to resolve ESLint errors
🔧 ESLint Fixes:
- Remove unused 'categoryConfig' variable in Layout.js (line 167)
- Remove unused 'isHome' variable in ToolSidebar.js (line 77)
- Remove unused 'Sparkles' import in tools.js (line 1)

 Build Status:
- All ESLint errors resolved
- Build now compiles successfully
- Ready for deployment

📁 Files Modified:
- /src/components/Layout.js - Removed unused categoryConfig for NON_TOOLS
- /src/components/ToolSidebar.js - Removed unused isHome for NON_TOOLS
- /src/config/tools.js - Removed unused Sparkles import
2025-09-24 19:21:28 +07:00
dwindown
7792190ea1 feat: Enhanced What's New feature with NON_TOOLS category and global footer
 What's New Feature & Navigation Improvements:
- Added attractive 'What's New' button to homepage with gradient design and sparkle effects
- Created NON_TOOLS category for better navigation organization (Home, What's New)
- Separated navigation items in sidebar and mobile menu with clear visual hierarchy
- Implemented unified global footer across all pages for consistency

🎨 Design Enhancements:
- Stunning gradient button with indigo→purple→pink colors and hover animations
- Perfect placement between stats and tools grid for maximum visibility
- Consistent indigo-purple theming for non-tools category
- Professional sparkle effects and scale transforms on hover

🔧 Technical Improvements:
- Removed duplicate footer from Terms of Service page
- Unified footer implementation reduces code duplication
- Enhanced mobile dropdown with proper NON_TOOLS separation
- Updated sidebar with category-based styling and separators

📁 Files Modified:
- /src/config/tools.js - Added NON_TOOLS category and What's New entry
- /src/components/ToolSidebar.js - Separated NON_TOOLS with visual hierarchy
- /src/components/Layout.js - Updated mobile menu and implemented global footer
- /src/pages/Home.js - Added attractive What's New button with animations
- /src/pages/TermsOfService.js - Removed duplicate footer
- /src/pages/ReleaseNotes.js - Updated with latest implementation details
2025-09-24 19:02:12 +07:00
dwindown
21d0406ece Improve ObjectEditor and PostmanTable UI/UX
- Enhanced JSON parsing with smart error handling for trailing commas
- Fixed StructuredEditor array handling - array indices now non-editable
- Improved PostmanTable styling: removed border radius, solid thead background
- Enhanced icon spacing and alignment for better visual hierarchy
- Added copy feedback system with green check icons (2500ms duration)
- Restructured MindmapView node layout with dedicated action column
- Added HTML rendering toggle for PostmanTable similar to MindmapView
- Implemented consistent copy functionality across components
- Removed debug console.log statements for cleaner codebase
2025-09-24 14:05:10 +07:00
dwindown
57655410ab feat: optimize analytics and mobile UI improvements
Analytics & Consent Optimization:
- Auto-grant all consent for non-EEA users (maximize analytics data)
- EEA users still see consent banner (GDPR compliant)
- Removed debugging console logs from consent system
- Analytics now works in both development and production

Mobile UI Improvements:
- Fixed feature list layout on homepage (responsive flex layout)
- Improved consent banner button styling (better padding, full-width on mobile)
- Fixed mobile dropdown menu positioning (now sticky to header with overlay)
- Enhanced mobile navigation UX with proper z-index and backdrop

Legal Compliance:
- EEA users: Explicit consent required (GDPR compliant)
- Non-EEA users: Automatic tracking (legal, maximizes data collection)
- Maintains privacy-first approach while optimizing analytics coverage
2025-09-24 01:15:20 +07:00
dwindown
2e67a2bca2 feat: comprehensive SEO optimization and GDPR compliance
- Added Terms of Service and Privacy Policy pages with contact info
- Implemented Google Analytics with Consent Mode v2 for GDPR compliance
- Created sitemap.xml and robots.txt for search engine optimization
- Added dynamic meta tags, Open Graph, and structured data (JSON-LD)
- Implemented GDPR consent banner with TCF 2.2 compatibility
- Enhanced sidebar with category-colored hover states and proper active/inactive styling
- Fixed all ESLint warnings for clean deployment
- Added comprehensive SEO utilities and privacy-first analytics tracking

Ready for production deployment with full legal compliance and SEO optimization.
2025-09-24 00:12:28 +07:00
dwindown
dd03a7213f Remove all remaining unused variables and ESLint warnings
- Removed unused functions: handleClearConfirm, handleClearCancel, exportToJson, exportToCsv
- Removed unused variable: allDecimals and its references
- Added ESLint disable comment for error variable (used in UI but not detected by linter)
- Build now compiles successfully with zero warnings or errors
- Ready for CI/CD deployment in Coolify
2025-09-23 15:30:05 +07:00
dwindown
5f6aa2210a Fix ESLint errors for successful deployment
- Fixed mixed operators in contentExtractor.js with proper parentheses
- Removed unused variables and imports across all components
- Fixed useCallback dependencies in ObjectEditor.js
- Corrected == to === comparisons in TableEditor.js
- Fixed undefined variable references
- Wrapped serializeToPhp in useCallback to resolve dependency warning
- Updated table column width styling from min-w to w for consistent layout

Build now passes successfully with only non-blocking warnings remaining.
2025-09-23 14:46:30 +07:00
dwindown
977e784df2 Improve ObjectEditor and Add TableEditor 2025-09-23 14:17:13 +07:00
dwindown
cf750114f7 🐛 Remove all setSelectedRowIndex references
- Remove setSelectedRowIndex calls from handleRowClick
- Remove setSelectedRowIndex calls from handleBack
- Remove setSelectedRowIndex calls from handleBreadcrumbClick
- Build now passes ESLint validation 
2025-09-21 17:29:46 +07:00
dwindown
ece5ffc63f 🐛 Fix ESLint errors in PostmanTable.js
- Remove unused useMemo import
- Remove unused selectedRowIndex state
- Remove unused isComplexValue function
- Fix CI deployment build failure
2025-09-21 17:27:27 +07:00
dwindown
f2163c9814 Enhanced Object Editor with fetch data & mobile improvements
🚀 New Features:
- Fetch Data functionality with URL input and error handling
- Auto-protocol detection (adds https:// if missing)
- Smart content-type handling for various JSON APIs
- Perfect for Telegram Bot API, GitHub API, JSONPlaceholder, etc.

📱 Mobile Responsiveness:
- Desktop: Clean tab interface for view modes
- Mobile: Native select dropdown with emoji icons
- StructuredEditor: Horizontal scroll for wide JSON structures
- Input data field auto-hides on successful fetch

🐛 Critical Fixes:
- Fixed StructuredEditor reinitialization loop issue
- Fixed deep nested property/array item deletion
- Proper array splicing and object property removal
- Internal vs external data change tracking

🎨 UX Improvements:
- Loading states during fetch operations
- Better error messages and validation
- Responsive button layouts and spacing
- Enhanced usage tips with fetch examples
2025-09-21 17:08:20 +07:00
85 changed files with 26580 additions and 2113 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

226
EDITOR_CHECKLIST.md Normal file
View File

@@ -0,0 +1,226 @@
# Editor Tool Consistency Checklist
Quick reference for ensuring your new editor tool follows all consistency patterns.
## ✅ Input Section
### Tab Structure
- [ ] 4 tabs: Create New, URL, Paste, Open
- [ ] Icons: Plus, Globe, FileText, Upload
- [ ] Active tab styling: `bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500`
### Create New Tab
- [ ] Grid layout with 2 buttons (Start Empty, Load Sample)
- [ ] Dashed border buttons with icons
- [ ] Blue tip box at bottom
### URL Tab ⚠️ CRITICAL
- [ ] `<div className="flex gap-2">` wrapper
- [ ] Input in `<div className="relative flex-1">`
- [ ] Button INLINE on the right (not below!)
- [ ] Button text: "Fetch Data" or "Fetching..."
- [ ] Enter key triggers fetch
- [ ] Helper text below
### Paste Tab
- [ ] CodeMirrorEditor component
- [ ] Collapse after successful parse
- [ ] Collapsed state shows summary with "Edit Input ▼" button
- [ ] Error display in red box
- [ ] Parse button on bottom-right
- [ ] Button disabled when invalid
### Open Tab ⚠️ CRITICAL
- [ ] `<input className="tool-input" />` (not custom styling!)
- [ ] Auto-load on file selection (no button needed!)
- [ ] Green privacy notice below
---
## ✅ Main Editor Section
- [ ] Conditional render: `{(activeTab !== 'create' || createNewCompleted) && ...}`
- [ ] White card with border
- [ ] Header with icon and title
- [ ] Content area with padding
### Fullscreen (Optional)
- [ ] `z-[99999]` when fullscreen
- [ ] `!m-0` and `marginTop: "0 !important"` to override spacing
---
## ✅ Export Section
- [ ] Collapsible by default (`exportExpanded` state)
- [ ] Clickable header with hover effect
- [ ] ChevronUp/ChevronDown icons
- [ ] Summary info in header
- [ ] Content only renders when expanded
---
## ✅ Usage Tips
- [ ] Collapsible by default (`usageTipsExpanded` state)
- [ ] Header: "💡 Usage Tips"
- [ ] Clickable header with hover effect
- [ ] ChevronUp/ChevronDown icons
- [ ] Organized by categories:
- [ ] 📝 Input Methods
- [ ] ✏️ Editing Features (editor-specific)
- [ ] 📤 Export Options
- [ ] 💾 Data Privacy
---
## ✅ Data Loss Prevention (CRITICAL!)
### Confirmation Modal
- [ ] Shows when user has data and tries to switch tabs
- [ ] Shows when user clicks Start Empty/Load Sample with existing data
- [ ] Amber header with AlertTriangle icon
- [ ] Lists specific data user will lose (not generic)
- [ ] Blue tip box suggesting to save first
- [ ] "Cancel" button (gray)
- [ ] "Switch & Clear Data" button (amber with icon)
- [ ] z-50 to appear above everything
### Logic
- [ ] `hasUserData()` function implemented
- [ ] `hasModifiedData()` function implemented
- [ ] `handleTabChange()` checks for data before switching
- [ ] `showInputChangeModal` state variable
- [ ] `pendingTabChange` state variable
- [ ] Modal component created as separate component
- [ ] `onConfirm` clears data and switches tab
- [ ] `onCancel` closes modal without action
---
## ✅ State Variables
```jsx
// Required states
const [activeTab, setActiveTab] = useState('create');
const [createNewCompleted, setCreateNewCompleted] = useState(false);
const [inputText, setInputText] = useState('');
const [url, setUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [pasteCollapsed, setPasteCollapsed] = useState(false);
const [pasteDataSummary, setPasteDataSummary] = useState(null);
const [exportExpanded, setExportExpanded] = useState(false);
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
const [showInputChangeModal, setShowInputChangeModal] = useState(false);
const [pendingTabChange, setPendingTabChange] = useState(null);
const fileInputRef = useRef(null);
```
---
## ✅ Error Handling
- [ ] Parse errors keep input expanded
- [ ] Parse errors show in red box with AlertTriangle icon
- [ ] Valid data collapses input and shows editor
- [ ] Error state: `setPasteCollapsed(false)`
- [ ] Success state: `setPasteCollapsed(true)`
---
## ✅ Imports
```jsx
import {
Plus, Globe, FileText, Upload, // Input tabs
Download, Edit3, // Editor/Export
AlertTriangle, // Errors
ChevronUp, ChevronDown, // Collapse indicators
} from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
```
---
## ✅ Visual Consistency
### Colors
- [ ] Primary action: `bg-blue-600 hover:bg-blue-700`
- [ ] Disabled: `disabled:bg-gray-400`
- [ ] Success: `bg-green-50 dark:bg-green-900/20`
- [ ] Error: `bg-red-50 dark:bg-red-900/20`
- [ ] Info: `bg-blue-50 dark:bg-blue-900/20`
### Spacing
- [ ] Section spacing: `mt-6`
- [ ] Internal spacing: `space-y-3`
- [ ] Padding: `p-4` or `px-4 py-3`
### Typography
- [ ] Headers: `text-lg font-semibold`
- [ ] Body: `text-sm`
- [ ] Helper text: `text-xs`
---
## ✅ Behavior
- [ ] Tab change confirmation when data exists
- [ ] File input auto-loads (no button)
- [ ] Paste requires button click
- [ ] URL supports Enter key
- [ ] Collapsible sections start collapsed
- [ ] Error messages are specific and helpful
---
## ⚠️ Common Mistakes to Avoid
1. ❌ URL button below input → ✅ Button inline on right
2. ❌ Custom file input styling → ✅ Use `tool-input` class
3. ❌ File input with parse button → ✅ Auto-load on selection
4. ❌ Always-visible usage tips → ✅ Collapsible by default
5. ❌ Always-visible export → ✅ Collapsible by default
6. ❌ Parse button on left → ✅ Parse button on right
7. ❌ No error handling → ✅ Show errors, keep input expanded
---
## 🧪 Testing Checklist
- [ ] Create New: Both buttons work
- [ ] URL: Inline button, Enter key works
- [ ] Paste: Valid data collapses, invalid shows error
- [ ] Open: Auto-loads file
- [ ] Export: Collapses/expands correctly
- [ ] Usage Tips: Collapses/expands correctly
- [ ] Tab switching: Confirmation modal works
- [ ] Dark mode: All colors work
- [ ] Mobile: Responsive layout
- [ ] Errors: Specific messages, input stays expanded
---
## 📚 Reference Examples
- **Object Editor**: `/src/pages/ObjectEditor.js`
- **Table Editor**: `/src/pages/TableEditor.js`
- **Invoice Editor**: `/src/pages/InvoiceEditor.js`
---
## 🚀 Quick Start
1. Copy structure from existing editor
2. Replace editor-specific logic
3. Update Usage Tips content
4. Test all input methods
5. Test collapse/expand behavior
6. Test error handling
7. Test dark mode
8. Test mobile view
**Follow this checklist and your editor will be perfectly consistent!**

748
EDITOR_TOOL_GUIDE.md Normal file
View File

@@ -0,0 +1,748 @@
# Editor Tool Building Guide
This guide ensures consistency across all editor tools in the Developer Tools application. Follow these patterns when creating new editor tools.
## Table of Contents
1. [Input Section Patterns](#input-section-patterns)
2. [Main Editor Section](#main-editor-section)
3. [Export/Output Section](#export-output-section)
4. [Usage Tips Section](#usage-tips-section)
5. [State Management](#state-management)
6. [Error Handling](#error-handling)
7. [Complete Example](#complete-example)
---
## Input Section Patterns
### Required Tabs
All editor tools must have these 4 input tabs:
1. **Create New** - Start empty or load sample data
2. **URL** - Fetch data from API endpoints
3. **Paste** - Paste data directly
4. **Open** - Upload files
### Tab Structure
```jsx
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button onClick={() => handleTabChange('create')} className={...}>
<Plus className="h-4 w-4" />
Create New
</button>
<button onClick={() => handleTabChange('url')} className={...}>
<Globe className="h-4 w-4" />
URL
</button>
<button onClick={() => handleTabChange('paste')} className={...}>
<FileText className="h-4 w-4" />
Paste
</button>
<button onClick={() => handleTabChange('open')} className={...}>
<Upload className="h-4 w-4" />
Open
</button>
</div>
```
### Create New Tab
```jsx
{activeTab === 'create' && (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Start Empty Button */}
<button onClick={handleStartEmpty} className="flex flex-col items-center p-6 border-2 border-dashed...">
<Plus className="h-8 w-8..." />
<span className="font-medium...">Start Empty</span>
<span className="text-xs...">Create a blank structure</span>
</button>
{/* Load Sample Button */}
<button onClick={handleLoadSample} className="flex flex-col items-center p-6 border-2 border-dashed...">
<FileText className="h-8 w-8..." />
<span className="font-medium...">Load Sample</span>
<span className="text-xs...">Start with example data</span>
</button>
</div>
{/* Tip */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<p className="text-xs text-blue-700 dark:text-blue-300">
💡 <strong>Tip:</strong> You can always import data later using the URL, Paste, or Open tabs.
</p>
</div>
</div>
)}
```
### URL Tab (CONSISTENT LAYOUT)
**IMPORTANT:** Button must be INLINE on the right, not below!
```jsx
{activeTab === 'url' && (
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://api.telegram.org/bot<token>/getMe"
className="tool-input w-full"
onKeyPress={(e) => e.key === 'Enter' && handleFetchData()}
/>
</div>
<button
onClick={handleFetchData}
disabled={isLoading || !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"
>
{isLoading ? 'Fetching...' : 'Fetch Data'}
</button>
</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>
</div>
)}
```
### Paste Tab (WITH COLLAPSE)
```jsx
{activeTab === 'paste' && (
pasteCollapsed ? (
// Collapsed State
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="flex items-center justify-between">
<span className="text-sm text-green-700 dark:text-green-300">
Data loaded: {pasteDataSummary.format} ({pasteDataSummary.size.toLocaleString()} chars)
</span>
<button
onClick={() => setPasteCollapsed(false)}
className="text-sm text-blue-600 hover:underline"
>
Edit Input
</button>
</div>
</div>
) : (
// Expanded State
<div className="space-y-3">
<div>
<CodeMirrorEditor
value={inputText}
onChange={setInputText}
language="json"
placeholder="Paste your data here..."
maxLines={12}
showToggle={true}
className="w-full"
/>
</div>
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-600 dark:text-red-400">
<strong>Invalid Data:</strong> {error}
</p>
</div>
)}
<div className="flex items-center justify-between flex-shrink-0">
<div className="text-sm text-gray-600 dark:text-gray-400">
Auto-detects format
</div>
<button
onClick={handleParseData}
disabled={!inputValid || !inputText.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded-md transition-colors flex-shrink-0"
>
Parse Data
</button>
</div>
</div>
)
)}
```
### Open Tab (CONSISTENT WITH TABLE EDITOR)
**IMPORTANT:** Use `tool-input` class and auto-load on file selection!
```jsx
{activeTab === 'open' && (
<div className="space-y-3">
<input
ref={fileInputRef}
type="file"
accept=".json,.txt"
onChange={handleFileImport}
className="tool-input"
/>
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md p-3">
<p className="text-xs text-green-700 dark:text-green-300">
🔒 <strong>Privacy:</strong> Your data stays in your browser. We don't store or upload anything - just help you open, edit, and export your files locally.
</p>
</div>
</div>
)}
```
### Parse Button Behavior
- **Valid data**: Collapse input, show summary, display editor
- **Invalid data**: Show error, keep input expanded, hide editor
- **Button disabled**: When no data or invalid format
---
## Main Editor Section
### Structure
```jsx
{(activeTab !== 'create' || createNewCompleted) && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
{/* Editor Header */}
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Edit3 className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Your Editor Name
</h3>
{/* Optional: Settings or Actions */}
</div>
</div>
{/* Editor Content */}
<div className="p-4">
{/* Your editor implementation */}
</div>
</div>
)}
```
### Fullscreen Support (Optional)
If your editor supports fullscreen:
```jsx
const [isFullscreen, setIsFullscreen] = useState(false);
<div className={`... ${
isFullscreen
? "fixed inset-0 z-[99999] rounded-none border-0 shadow-none overflow-hidden !m-0"
: "..."
}`}
style={isFullscreen ? { marginTop: "0 !important" } : {}}
>
```
---
## Export/Output Section
### Collapsible Export (REQUIRED)
```jsx
{data && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
{/* Collapsible Header */}
<div
onClick={() => setExportExpanded(!exportExpanded)}
className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Download className="h-5 w-5 text-green-600 dark:text-green-400" />
Export Results
{exportExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</h3>
<div className="text-sm text-gray-600 dark:text-gray-400">
{/* Summary info */}
</div>
</div>
</div>
{/* Export Content - Collapsible */}
{exportExpanded && (
<div>
{/* Export tabs and content */}
</div>
)}
</div>
)}
```
### Export Tabs Pattern
```jsx
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setExportTab('json')}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
exportTab === 'json'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Braces className="h-4 w-4" />
JSON
</button>
{/* More export format tabs */}
</div>
```
---
## Usage Tips Section
### Collapsible Usage Tips (REQUIRED)
```jsx
{/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md overflow-hidden mt-6">
<div
onClick={() => setUsageTipsExpanded(!usageTipsExpanded)}
className="px-4 py-3 cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors flex items-center justify-between"
>
<h4 className="text-blue-800 dark:text-blue-200 font-medium flex items-center gap-2">
💡 Usage Tips
</h4>
{usageTipsExpanded ? <ChevronUp className="h-4 w-4 text-blue-600 dark:text-blue-400" /> : <ChevronDown className="h-4 w-4 text-blue-600 dark:text-blue-400" />}
</div>
{usageTipsExpanded && (
<div className="px-4 pb-4 text-blue-700 dark:text-blue-300 text-sm space-y-3">
<div>
<p className="font-medium mb-1">📝 Input Methods:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Create New:</strong> Description</li>
<li><strong>URL Import:</strong> Description</li>
<li><strong>Paste Data:</strong> Description</li>
<li><strong>Open Files:</strong> Description</li>
</ul>
</div>
<div>
<p className="font-medium mb-1">✏️ Editing Features:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
{/* Your editor-specific features */}
</ul>
</div>
<div>
<p className="font-medium mb-1">📤 Export Options:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
{/* Your export formats */}
</ul>
</div>
<div>
<p className="font-medium mb-1">💾 Data Privacy:</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Local Processing:</strong> All data stays in your browser</li>
<li><strong>No Upload:</strong> We don't store or transmit your data</li>
<li><strong>Secure:</strong> Your information remains private</li>
</ul>
</div>
</div>
)}
</div>
```
---
## Data Loss Prevention (CRITICAL!)
### Confirmation Modal for Tab Changes
**IMPORTANT:** Always confirm before clearing user data when switching input methods!
### When to Show Confirmation
- User has entered/loaded data
- User tries to switch to a different input tab
- User tries to use Create New buttons (Start Empty/Load Sample)
### Implementation Pattern
#### 1. Check if User Has Data
```jsx
const hasUserData = () => {
// Check if there's meaningful data
return Object.keys(editorData).length > 0;
};
const hasModifiedData = () => {
// Check if data has been modified from initial state
// Return false for empty or sample data
// Return true for user-entered data
};
```
#### 2. Handle Tab Change with Confirmation
```jsx
const handleTabChange = (newTab) => {
if (hasModifiedData() && activeTab !== newTab) {
setPendingTabChange(newTab);
setShowInputChangeModal(true);
} else {
setActiveTab(newTab);
}
};
```
#### 3. Confirmation Modal Component
```jsx
const InputChangeConfirmationModal = ({
editorData,
currentMethod,
newMethod,
onConfirm,
onCancel
}) => {
const getMethodName = (method) => {
switch (method) {
case 'create': return 'Create New';
case 'create_empty': return 'Start Empty';
case 'create_sample': return 'Load Sample';
case 'url': return 'URL Import';
case 'paste': return 'Paste Data';
case 'open': return 'File Upload';
default: return method;
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full overflow-hidden">
{/* Header - Amber Warning */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-amber-50 dark:bg-amber-900/20">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-amber-800 dark:text-amber-200">
Confirm Action
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300">
{newMethod === 'create_empty' || newMethod === 'create_sample'
? `Using ${getMethodName(newMethod)} will clear your current data.`
: `Switching from ${getMethodName(currentMethod)} to ${getMethodName(newMethod)} will clear your current data.`
}
</p>
</div>
</div>
</div>
{/* Body - Show Current Data Summary */}
<div className="px-6 py-4">
<div className="space-y-3">
<p className="text-sm text-gray-600 dark:text-gray-400">
You currently have:
</p>
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-1 ml-4">
{/* List current data items */}
<li> [Data summary item 1]</li>
<li> [Data summary item 2]</li>
<li> [Data summary item 3]</li>
</ul>
{/* Blue Tip Box */}
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-xs text-blue-700 dark:text-blue-300">
<strong>Tip:</strong> Consider downloading your current data as JSON before proceeding to save your work.
</p>
</div>
</div>
</div>
{/* Footer - Action Buttons */}
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700/50 flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-500 transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 rounded-md transition-colors flex items-center gap-2"
>
<AlertTriangle className="h-4 w-4" />
Switch & Clear Data
</button>
</div>
</div>
</div>
);
};
```
#### 4. Usage in Component
```jsx
{/* Confirmation Modal */}
{showInputChangeModal && (
<InputChangeConfirmationModal
editorData={editorData}
currentMethod={activeTab}
newMethod={pendingTabChange}
onConfirm={() => {
clearAllData();
setActiveTab(pendingTabChange);
setShowInputChangeModal(false);
setPendingTabChange(null);
}}
onCancel={() => {
setShowInputChangeModal(false);
setPendingTabChange(null);
}}
/>
)}
```
### Visual Design
- **Header**: Amber background with AlertTriangle icon
- **Body**: White/dark background with data summary
- **Tip Box**: Blue background with save suggestion
- **Footer**: Gray background with Cancel and Warning buttons
- **Warning Button**: Amber color with AlertTriangle icon
### Key Points
1. **Always show** when user has meaningful data
2. **List specific data** user will lose (not generic message)
3. **Suggest saving** before proceeding (blue tip box)
4. **Clear button text** - "Switch & Clear Data" (not ambiguous)
5. **z-50** to appear above everything
6. **Amber theme** for warning (not red, not blue)
---
## State Management
### Required State Variables
```jsx
// Tab management
const [activeTab, setActiveTab] = useState('create');
const [createNewCompleted, setCreateNewCompleted] = useState(false);
// Input data
const [inputText, setInputText] = useState('');
const [url, setUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
// Collapse states
const [pasteCollapsed, setPasteCollapsed] = useState(false);
const [pasteDataSummary, setPasteDataSummary] = useState(null);
const [exportExpanded, setExportExpanded] = useState(false);
const [usageTipsExpanded, setUsageTipsExpanded] = useState(false);
// Confirmation modals
const [showInputChangeModal, setShowInputChangeModal] = useState(false);
const [pendingTabChange, setPendingTabChange] = useState(null);
// Refs
const fileInputRef = useRef(null);
// Your editor-specific state
const [editorData, setEditorData] = useState(null);
```
### Tab Change with Confirmation
```jsx
const handleTabChange = (newTab) => {
if (hasModifiedData() && activeTab !== newTab) {
setPendingTabChange(newTab);
setShowInputChangeModal(true);
} else {
setActiveTab(newTab);
}
};
const confirmInputChange = () => {
clearAllData();
setActiveTab(pendingTabChange);
setShowInputChangeModal(false);
setPendingTabChange(null);
};
```
---
## Error Handling
### Parse Data with Error Handling
```jsx
const handleParseData = () => {
try {
const parsed = parseYourData(inputText);
if (parsed.valid) {
setEditorData(parsed.data);
setError('');
setCreateNewCompleted(true);
setPasteDataSummary({
format: parsed.format,
size: inputText.length,
items: parsed.data.length
});
setPasteCollapsed(true);
} else {
// Show error, keep input expanded
setError(parsed.error || 'Invalid data format');
setPasteCollapsed(false);
}
} catch (err) {
setError(err.message || 'Failed to parse data');
setPasteCollapsed(false);
}
};
```
### Error Display
```jsx
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-red-600 dark:text-red-400" />
<p className="text-red-700 dark:text-red-300">{error}</p>
</div>
</div>
)}
```
---
## Complete Example
See existing editors for complete implementations:
- **Object Editor** (`/src/pages/ObjectEditor.js`) - Best for JSON/structured data
- **Table Editor** (`/src/pages/TableEditor.js`) - Best for tabular data
- **Invoice Editor** (`/src/pages/InvoiceEditor.js`) - Best for form-based editors
---
## Checklist for New Editor Tools
### Input Section
- [ ] All 4 tabs implemented (Create New, URL, Paste, Open)
- [ ] URL tab has inline button (not below)
- [ ] Open tab uses `tool-input` class
- [ ] Open tab auto-loads on file selection
- [ ] Paste tab has parse button on bottom-right
- [ ] Paste tab collapses after successful parse
- [ ] Error handling shows errors and keeps input expanded
### Main Editor
- [ ] Only shows after data is loaded
- [ ] Has proper header with icon
- [ ] Implements your editor functionality
- [ ] Optional: Fullscreen support with proper z-index
### Export Section
- [ ] Collapsible by default
- [ ] Header shows summary info
- [ ] Chevron icon indicates state
- [ ] Hover effect on header
- [ ] Export tabs if multiple formats
### Usage Tips
- [ ] Collapsible by default
- [ ] 💡 emoji in header
- [ ] Comprehensive tips organized by category
- [ ] Includes Input Methods, Editing Features, Export Options, Data Privacy
### State & Behavior
- [ ] Tab change confirmation when data exists
- [ ] Proper error handling
- [ ] Loading states for async operations
- [ ] Dark mode support
### Styling
- [ ] Uses Tailwind CSS classes
- [ ] Consistent with other editors
- [ ] Responsive design
- [ ] Proper spacing and layout
---
## Common Patterns
### Button Styling
```jsx
// Primary action button
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-medium px-4 py-2 rounded-md transition-colors flex-shrink-0"
// Secondary button
className="bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 px-4 py-2 rounded-md transition-colors"
```
### Input Styling
```jsx
// Standard input
className="tool-input w-full"
// Or explicit:
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
```
### Card/Section Styling
```jsx
className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden"
```
---
## Import Requirements
### Required Icons (from lucide-react)
```jsx
import {
Plus, // Create New
Globe, // URL
FileText, // Paste
Upload, // Open
Download, // Export
Edit3, // Editor
AlertTriangle, // Errors
ChevronUp, // Collapse indicators
ChevronDown, // Collapse indicators
// Add your specific icons
} from 'lucide-react';
```
### Required Components
```jsx
import ToolLayout from '../components/ToolLayout';
import CodeMirrorEditor from '../components/CodeMirrorEditor';
import CodeEditor from '../components/CodeEditor'; // For read-only code display
```
---
## Best Practices
1. **Consistency First**: Always follow existing patterns before innovating
2. **User Data Safety**: Always confirm before clearing user data
3. **Error Messages**: Be specific and helpful
4. **Loading States**: Show loading indicators for async operations
5. **Accessibility**: Use semantic HTML and proper ARIA labels
6. **Dark Mode**: Test in both light and dark modes
7. **Mobile**: Ensure responsive design works on small screens
8. **Performance**: Lazy load heavy components when possible
---
## Testing Your Editor
1. **Input Methods**: Test all 4 tabs work correctly
2. **Parse Valid Data**: Ensure data loads and displays
3. **Parse Invalid Data**: Ensure errors show and input stays expanded
4. **Tab Switching**: Confirm data loss prevention works
5. **Export**: Test all export formats
6. **Collapse/Expand**: Test all collapsible sections
7. **Dark Mode**: Toggle and verify styling
8. **Mobile**: Test on small screen sizes
9. **Edge Cases**: Empty data, very large data, special characters
---
## Questions or Issues?
Refer to existing editors:
- Object Editor: JSON/PHP serialized data handling
- Table Editor: Tabular data with inline editing
- Invoice Editor: Form-based data entry
Follow these patterns and your new editor will be consistent with the rest of the application!

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

490
PROJECT_ROADMAP.md Normal file
View File

@@ -0,0 +1,490 @@
# Developer Tools - Project Roadmap
**Last Updated:** October 22, 2025
---
## 🎯 Vision
Build a comprehensive suite of developer tools with a focus on:
1. **Privacy-First** - All processing in browser
2. **User-Friendly** - Intuitive UI/UX
3. **Monetization** - Ad-supported with optional ad-free experience
4. **Quality** - Consistent patterns across all tools
---
## 📊 Current Status
### ✅ Completed Tools (8 Active Tools)
- **Markdown Editor** - Write & preview markdown with live rendering ✨ NEW
- **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
- **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
- Consistent input patterns (Create/URL/Paste/Open)
- Collapsible sections (Export, Usage Tips)
- Data loss prevention (confirmation modals)
- Dark mode support
- Responsive design
- SEO optimization (FAQ schema, breadcrumbs, internal linking)
- Related tools recommendations
- Custom 404 page
- Documentation (EDITOR_TOOL_GUIDE.md, EDITOR_CHECKLIST.md)
---
## 🚀 Roadmap
### Phase 1: Foundation & Monetization (Current - Week 1-2)
#### Priority 1: Advanced URL Fetch ⭐
**Status:** ✅ Completed (Hidden for PRO tier)
**Timeline:** 1-2 days
**Impact:** HIGH - Benefits all existing and future tools
**Features:**
- Simple mode (current: just URL + GET)
- Advanced mode (toggle):
- HTTP method selector (GET, POST, PUT, DELETE, PATCH)
- Custom headers (key-value pairs)
- Request body editor (JSON/form-data)
- Query parameters builder
- Auth presets (Bearer, Basic, API Key)
- Save/load request presets
- Response headers display
- Status code display
**Benefits:**
- Users can fetch from authenticated APIs
- Support for private company APIs
- GitHub API with auth tokens
- GraphQL endpoints
- Any REST API with custom requirements
**Implementation Notes:**
- Add to all existing editors (Object, Table, Invoice)
- Reusable component: `AdvancedURLFetch.jsx`
- Store presets in localStorage
- Validate JSON in request body
---
#### Priority 2: Ad Space Preparation 💰
**Status:** ✅ Completed (Placeholder ads ready)
**Timeline:** 1 day
**Impact:** HIGH - Start monetization immediately
**Desktop Layout:**
```
┌────────────────────────────┬─────────┐
│ │ [Ad] │
│ Main Content │ │
│ (Tool Editor) │ 300px │
│ │ │
│ │ [Ad] │
│ │ │
│ │ [Ad] │
└────────────────────────────┴─────────┘
```
**Mobile Layout:**
```
┌─────────────────────────┐
│ │
│ Main Content │
│ (Scrollable) │
│ │
├─────────────────────────┤
│ [Ad Banner 320x50] │ ← Sticky
└─────────────────────────┘
```
**Specifications:**
- **Desktop:**
- 300px fixed width right column
- Fixed positioning (ads stay visible)
- 3 ad blocks maximum
- Ad sizes: All 300x250 (Medium Rectangle) - Google AdSense policy compliant
- Hide below 1200px viewport width
- Main content: `calc(100% - 320px)`
- **Mobile:**
- 320x50 or 320x100 banner
- Sticky bottom position
- Close button for better UX
- Add padding-bottom to content
**Implementation:**
- Create `AdColumn.jsx` component
- Create `AdBlock.jsx` component
- Create `MobileAdBanner.jsx` component
- Update `ToolLayout.jsx` to include ad spaces
- Add responsive breakpoints
- Test with placeholder ads first
---
#### Priority 3: AdSense Integration 💵
**Status:** ⏳ In Progress (Awaiting approval)
**Timeline:** 1 day
**Impact:** HIGH - Start earning revenue
**Steps:**
1. Apply for Google AdSense account
2. Add AdSense script to `index.html`
3. Create ad units in AdSense dashboard
4. Implement ad components with AdSense code
5. Test ad display and responsiveness
6. Monitor ad performance
**Ad Units Needed:**
- Desktop Sidebar 1 (300x250)
- Desktop Sidebar 2 (300x250)
- Desktop Sidebar 3 (300x250)
- Mobile Bottom Banner (320x50)
**Compliance:**
- Add Privacy Policy page
- Add Terms of Service page
- Cookie consent banner (if required)
- GDPR compliance (if applicable)
---
### Phase 2: Content Expansion (Week 3-6)
#### Markdown Editor 📝
**Status:** ✅ Completed (October 22, 2025)
**Timeline:** 1-2 weeks
**Impact:** HIGH - Major new feature, attracts new users
**Core Features (MVP):**
- **Input Methods:**
- Create New (empty/sample)
- URL Import (fetch markdown from GitHub, Gist, etc.)
- Paste (markdown, HTML auto-convert, plain text)
- Open Files (.md, .txt, .html, .docx)
- **Editor:**
- CodeMirror with markdown syntax highlighting
- Split view (editor + live preview)
- View modes: Split, Editor Only, Preview Only, Fullscreen
- Markdown toolbar (Bold, Italic, Headers, Links, Images, Code, Lists, Tables)
- Line numbers
- Word count & statistics
- **Preview:**
- Live rendering (marked + DOMPurify)
- Syntax highlighting for code blocks (highlight.js)
- GitHub Flavored Markdown support
- Table of Contents auto-generation
- Mermaid diagram rendering (in preview)
- **Export:**
- Markdown (.md) - Standard, GFM, CommonMark
- HTML (.html) - Standalone with CSS
- Plain Text (.txt)
- PDF (.pdf) - via html2pdf
- DOCX (.docx) - via docx library
**Advanced Features (Post-MVP):**
- Tables support (GitHub-style)
- Task lists (checkboxes)
- Footnotes
- Emoji support (:smile:)
- Math equations (KaTeX)
- Templates (README, Documentation, Blog Post, etc.)
- Markdown linter
- Link checker
- Format beautifier
**Libraries:**
- `marked` or `markdown-it` - Markdown parser
- `turndown` - HTML to Markdown
- `mammoth.js` - DOCX to HTML/Markdown
- `html2pdf.js` - HTML to PDF
- `docx` - Generate DOCX files
- `highlight.js` - Code syntax highlighting
- `mermaid` - Diagram rendering
- `katex` - Math rendering (optional)
**Why Markdown Editor?**
- High demand from developers
- Complements existing tools
- Unique value: conversion hub
- Great for SEO
- Relatively straightforward to implement
---
#### Diagram Tool 🎨
**Status:** Planned
**Timeline:** 1-2 weeks
**Impact:** MEDIUM-HIGH - Separate tool, different audience
**Decision: SEPARATE from Markdown Editor**
**Why Separate:**
- Different use cases (documents vs diagrams)
- Different UX needs (text-heavy vs visual-heavy)
- Better specialization
- More SEO entry points
- Can be more powerful than mermaid alone
**Core Features (MVP):**
- **Input Methods:**
- Create New (empty/sample diagrams)
- URL Import (fetch mermaid/PlantUML code)
- Paste (mermaid code, PlantUML, etc.)
- Open Files (.mmd, .mermaid, .txt)
- **Editor:**
- Text-based editor (Mermaid syntax)
- Live preview
- Syntax highlighting
- Error detection
- **Diagram Types (Mermaid-based):**
- Flowchart
- Sequence Diagram
- Class Diagram
- State Diagram
- Entity Relationship Diagram
- Gantt Chart
- Pie Chart
- Git Graph
- **Export:**
- PNG (.png)
- SVG (.svg)
- PDF (.pdf)
- Mermaid Code (.mmd)
**Advanced Features (Post-MVP):**
- Visual editor (drag-and-drop nodes)
- Excalidraw-style drawing
- Draw.io-style interface
- Import from PlantUML
- Custom themes
- Collaboration features
**Libraries:**
- `mermaid` - Diagram rendering
- `html2canvas` - PNG export
- `svg-to-pdf` - PDF export
**Why Separate Diagram Tool?**
- Mermaid is powerful but limited
- Users want visual editing
- Can compete with Lucidchart, draw.io
- Different target audience
- Can be monetized separately
---
### Phase 3: Monetization Backend (Week 7+)
#### Payment Integration 💳
**Status:** Future
**Timeline:** 1 week
**Impact:** MEDIUM - Enables ad-free revenue
**Prerequisites:**
- Significant traffic (>1000 daily users)
- Ad revenue validated
- User requests for ad-free option
**Features:**
- One-time payment (no subscription)
- Multiple duration options
- Stripe/PayPal integration
- Simple checkout flow
- Email receipt
**Pricing Strategy (Recommended):**
- 1 month: $2.99
- 3 months: $6.99 (save 22%)
- 6 months: $11.99 (save 33%)
- 12 months: $19.99 (save 44%)
**Implementation:**
- Backend: Node.js + Express (or serverless functions)
- Database: PostgreSQL or MongoDB
- Payment: Stripe API
- Email: SendGrid or similar
---
#### User Accounts (Simple) 👤
**Status:** Future
**Timeline:** 1 week
**Impact:** MEDIUM - Required for ad-free tracking
**Features:**
- Email + password authentication
- OAuth (Google, GitHub) - optional
- Ad-free status tracking
- Purchase history
- Simple profile page
**No Complex Features:**
- No social features
- No data sync (keep tools client-side)
- No collaboration
- Just auth + payment tracking
**Implementation:**
- Auth: Firebase Auth or Auth0
- Database: Store user ID + ad-free expiry date
- Frontend: Check ad-free status, hide ads
---
#### Ad-Free Experience 🚫
**Status:** Future
**Timeline:** 3 days
**Impact:** MEDIUM - Premium user experience
**Features:**
- Hide all ads for paid users
- "Remove Ads" badge in UI
- Expiry reminder (7 days before)
- Easy renewal process
**Implementation:**
```jsx
const { user, isAdFree } = useAuth();
return (
<div className="flex gap-5">
<main className="flex-1">
{/* Tool content */}
</main>
{!isAdFree && <AdColumn />}
</div>
);
```
---
## 🎯 Success Metrics
### Phase 1 (Foundation & Monetization)
- [ ] Advanced URL Fetch used by >50% of users
- [ ] Ad impressions: >10,000/day
- [ ] Ad revenue: >$50/month
- [ ] No significant performance degradation
### Phase 2 (Content Expansion)
- [ ] Markdown Editor: >1,000 users/week
- [ ] Diagram Tool: >500 users/week
- [ ] Total tools: 5 (Object, Table, Invoice, Markdown, Diagram)
- [ ] Ad revenue: >$200/month
### Phase 3 (Monetization Backend)
- [ ] Ad-free purchases: >10/month
- [ ] Ad-free revenue: >$50/month
- [ ] Total revenue: >$250/month
- [ ] User satisfaction: >4.5/5 stars
---
## 📈 Growth Strategy
### SEO Optimization
- [ ] Optimize page titles and meta descriptions
- [ ] Add structured data (Schema.org)
- [ ] Create sitemap.xml
- [ ] Submit to Google Search Console
- [ ] Create blog posts about tools
- [ ] Create tutorial videos
- [ ] Build backlinks
### Content Marketing
- [ ] Write "How to" guides
- [ ] Create comparison articles (vs competitors)
- [ ] Share on Reddit (r/webdev, r/programming)
- [ ] Share on Hacker News
- [ ] Share on Twitter/X
- [ ] Share on LinkedIn
### Community Building
- [ ] Create Discord server (optional)
- [ ] Add feedback form
- [ ] Respond to user feedback
- [ ] Add feature request voting
- [ ] Create changelog page
---
## 🛠️ Technical Debt & Improvements
### Performance
- [ ] Implement code splitting
- [ ] Lazy load heavy components
- [ ] Optimize bundle size
- [ ] Add service worker (PWA)
- [ ] Implement caching strategies
### Testing
- [ ] Add unit tests (Jest)
- [ ] Add integration tests (React Testing Library)
- [ ] Add E2E tests (Playwright)
- [ ] Set up CI/CD pipeline
### Accessibility
- [ ] Add ARIA labels
- [ ] Test with screen readers
- [ ] Ensure keyboard navigation
- [ ] Add focus indicators
- [ ] Test color contrast
### Documentation
- [ ] Create API documentation (if applicable)
- [ ] Create user guides
- [ ] Create video tutorials
- [ ] Create FAQ page
---
## 📝 Notes
### Decision Log
- **2025-10-14**: Decided to separate Diagram Tool from Markdown Editor for better specialization
- **2025-10-14**: Prioritized Advanced URL Fetch over Markdown Editor for immediate impact
- **2025-10-14**: Chose ad-first monetization strategy with optional ad-free experience
- **2025-10-14**: Decided on one-time payment (no subscription) for ad-free
### Risks & Mitigation
- **Risk**: Ad revenue too low
- **Mitigation**: Focus on traffic growth, optimize ad placement
- **Risk**: Users use ad blockers
- **Mitigation**: Polite message asking to whitelist, offer ad-free option
- **Risk**: Competition from existing tools
- **Mitigation**: Focus on unique features (conversion hub, privacy-first)
- **Risk**: Backend costs too high
- **Mitigation**: Start with serverless, scale as needed
### Future Ideas (Backlog)
- Code Formatter (Prettier-based)
- JSON Diff Tool
- Base64 Encoder/Decoder
- URL Encoder/Decoder
- Hash Generator (MD5, SHA256, etc.)
- QR Code Generator
- Color Picker & Converter
- Regex Tester
- Cron Expression Generator
- JWT Decoder
- API Testing Tool (Postman-like)
---
**End of Roadmap**

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

888
TODO.md Normal file
View File

@@ -0,0 +1,888 @@
# Developer Tools - To-Do List
**Last Updated:** October 22, 2025
---
## 📋 Phase 1: Foundation & Monetization
### ⭐ Priority 1: Advanced URL Fetch (1-2 days) - ✅ COMPLETED
#### Design & Planning
- [ ] Design UI mockup for simple mode
- [ ] Design UI mockup for advanced mode
- [ ] Design toggle between modes
- [ ] Plan state management structure
- [ ] Plan localStorage schema for presets
#### Component Development
- [ ] Create `AdvancedURLFetch.jsx` component
- [ ] Create `HTTPMethodSelector.jsx` component
- [ ] Create `HeadersEditor.jsx` component (key-value pairs)
- [ ] Create `RequestBodyEditor.jsx` component (CodeMirror)
- [ ] Create `QueryParamsBuilder.jsx` component
- [ ] Create `AuthPresets.jsx` component
- [ ] Create `RequestPresetManager.jsx` component
#### Simple Mode
- [ ] Implement URL input field
- [ ] Implement GET request button
- [ ] Implement loading state
- [ ] Implement error handling
- [ ] Add helper text
#### Advanced Mode
- [ ] Implement mode toggle button
- [ ] Implement HTTP method selector (GET, POST, PUT, DELETE, PATCH)
- [ ] Implement custom headers input
- [ ] Add header button
- [ ] Remove header button
- [ ] Key-value pair inputs
- [ ] Validation
- [ ] Implement request body editor
- [ ] CodeMirror integration
- [ ] JSON syntax highlighting
- [ ] JSON validation
- [ ] Format button
- [ ] Implement query parameters builder
- [ ] Add param button
- [ ] Remove param button
- [ ] Key-value pair inputs
- [ ] Auto-append to URL
- [ ] Implement auth presets
- [ ] Bearer Token preset
- [ ] Basic Auth preset
- [ ] API Key preset
- [ ] Custom preset
#### Response Display
- [ ] Create response tabs (Body, Headers, Status)
- [ ] Implement response body display (formatted JSON)
- [ ] Implement response headers display
- [ ] Implement status code display with color coding
- [ ] Add response time display
- [ ] Add response size display
#### Presets Management
- [ ] Implement save preset functionality
- [ ] Implement load preset functionality
- [ ] Implement delete preset functionality
- [ ] Implement preset list UI
- [ ] Store presets in localStorage
- [ ] Add preset name input
- [ ] Add preset description (optional)
#### Integration
- [ ] Integrate into Object Editor
- [ ] Replace simple URL input
- [ ] Test with various APIs
- [ ] Update Usage Tips
- [ ] Integrate into Table Editor
- [ ] Replace simple URL input
- [ ] Test with various APIs
- [ ] Update Usage Tips
- [ ] Integrate into Invoice Editor
- [ ] Replace simple URL input
- [ ] Test with various APIs
- [ ] Update Usage Tips
#### Testing
- [ ] Test with GitHub API (requires auth)
- [ ] Test with JSONPlaceholder (public API)
- [ ] Test with Telegram Bot API
- [ ] Test with GraphQL endpoint
- [ ] Test with POST request + body
- [ ] Test with custom headers
- [ ] Test with query parameters
- [ ] Test error scenarios (404, 500, timeout)
- [ ] Test loading states
- [ ] Test preset save/load
- [ ] Test on mobile devices
- [ ] Test dark mode
#### Documentation
- [ ] Update EDITOR_TOOL_GUIDE.md with advanced URL pattern
- [ ] Add code examples
- [ ] Add screenshots
- [ ] Update EDITOR_CHECKLIST.md
---
### 💰 Priority 2: Ad Space Preparation (1 day) - ✅ COMPLETED
#### Design
- [ ] Finalize desktop ad column layout
- [ ] Finalize mobile ad banner layout
- [ ] Design placeholder ads for testing
- [ ] Plan responsive breakpoints
#### Desktop Ad Column
- [ ] Create `AdColumn.jsx` component
- [ ] 300px fixed width
- [ ] Sticky scroll behavior
- [ ] Space for 3 ad blocks
- [ ] Proper spacing between ads
- [ ] Create `AdBlock.jsx` component
- [ ] Support 300x250 size (all ads use this size for AdSense compliance)
- [ ] Placeholder content for testing
- [ ] Loading state
- [ ] Error state (if ad fails to load)
- [ ] Implement responsive behavior
- [ ] Show on screens >= 1200px
- [ ] Hide on screens < 1200px
- [ ] Smooth transition
#### Mobile Ad Banner
- [ ] Create `MobileAdBanner.jsx` component
- [ ] 320x50 or 320x100 size
- [ ] Sticky bottom position
- [ ] Close button
- [ ] Slide-in animation (optional)
- [ ] Implement close functionality
- [ ] Hide banner on close
- [ ] Store preference in localStorage (optional)
- [ ] Respect user choice
- [ ] Add padding-bottom to content
- [ ] Ensure content not hidden behind banner
- [ ] Dynamic padding based on banner height
#### Layout Integration
- [ ] Update `ToolLayout.jsx` to include ad spaces
- [ ] Add ad column container
- [ ] Add mobile banner container
- [ ] Maintain proper spacing
- [ ] Update main content width
- [ ] Desktop: `calc(100% - 320px)`
- [ ] Mobile: `100%`
- [ ] Test layout on all tool pages
- [ ] Object Editor
- [ ] Table Editor
- [ ] Invoice Editor
#### Testing
- [ ] Test desktop layout (1200px+)
- [ ] Test tablet layout (768px - 1199px)
- [ ] Test mobile layout (<768px)
- [ ] Test sticky scroll behavior
- [ ] Test sticky bottom banner
- [ ] Test close button functionality
- [ ] Test with placeholder ads
- [ ] Test dark mode compatibility
- [ ] Test on various screen sizes
- [ ] Test on actual devices (iOS, Android)
#### Performance
- [ ] Ensure no layout shift
- [ ] Optimize rendering performance
- [ ] Test scroll performance
- [ ] Measure impact on page load time
---
### 💵 Priority 3: AdSense Integration (1 day) - ⏳ IN PROGRESS
#### AdSense Setup
- [ ] Apply for Google AdSense account
- [ ] Provide website URL
- [ ] Provide contact information
- [ ] Wait for approval (can take 1-3 days)
- [ ] Verify site ownership (add verification code)
#### Ad Units Creation
- [ ] Log in to AdSense dashboard
- [ ] Create ad unit: Desktop Sidebar 1 (300x250)
- [ ] Create ad unit: Desktop Sidebar 2 (300x250)
- [ ] Create ad unit: Desktop Sidebar 3 (300x250)
- [ ] Create ad unit: Mobile Bottom Banner (320x50)
- [ ] Copy ad unit codes
#### Implementation
- [ ] Add AdSense script to `public/index.html`
```html
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXX"
crossorigin="anonymous"></script>
```
- [ ] Update `AdBlock.jsx` with AdSense code
```jsx
<ins className="adsbygoogle"
style={{ display: 'block' }}
data-ad-client="ca-pub-XXXXXXXX"
data-ad-slot="XXXXXXXXXX"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
```
- [ ] Update `MobileAdBanner.jsx` with AdSense code
- [ ] Initialize ads: `(adsbygoogle = window.adsbygoogle || []).push({});`
#### Testing
- [ ] Test ad display on desktop
- [ ] Test ad display on mobile
- [ ] Verify ads load correctly
- [ ] Check for console errors
- [ ] Test with ad blocker (should show message)
- [ ] Test on different browsers (Chrome, Firefox, Safari)
- [ ] Test on different devices
#### Monitoring
- [ ] Set up AdSense reporting
- [ ] Monitor ad impressions
- [ ] Monitor ad clicks
- [ ] Monitor ad revenue
- [ ] Track CTR (Click-Through Rate)
- [ ] Identify best-performing ad units
#### Compliance
- [ ] Create Privacy Policy page
- [ ] Data collection disclosure
- [ ] Cookie usage disclosure
- [ ] Third-party services (AdSense)
- [ ] User rights (GDPR)
- [ ] Create Terms of Service page
- [ ] Acceptable use policy
- [ ] Disclaimer
- [ ] Limitation of liability
- [ ] Add cookie consent banner (if required)
- [ ] Show on first visit
- [ ] Allow accept/decline
- [ ] Store preference
- [ ] Add "About Ads" link in footer
- [ ] Add "Privacy Policy" link in footer
- [ ] Add "Terms of Service" link in footer
#### Optimization
- [ ] Test different ad placements
- [ ] Test different ad sizes
- [ ] Monitor ad viewability
- [ ] Optimize for higher CTR
- [ ] A/B test ad positions (optional)
---
## 📋 Phase 2: Content Expansion
### 📝 Markdown Editor - MVP (1-2 weeks) - ✅ COMPLETED (Oct 22, 2025)
#### Planning
- [ ] Finalize feature list for MVP
- [ ] Design UI mockup (split view)
- [ ] Plan component structure
- [ ] Choose markdown parser (marked vs markdown-it)
- [ ] Plan export formats
#### Project Setup
- [ ] Create `MarkdownEditor.jsx` page
- [ ] Set up routing (`/markdown-editor`)
- [ ] Add to navigation menu
- [ ] Add to homepage tools list
#### Input Section
- [ ] Implement Create New tab
- [ ] Start Empty button
- [ ] Load Sample button (with example markdown)
- [ ] Tip box
- [ ] Implement URL tab
- [ ] Use AdvancedURLFetch component
- [ ] Support GitHub raw URLs
- [ ] Support Gist URLs
- [ ] Test with various markdown sources
- [ ] Implement Paste tab
- [ ] CodeMirror editor
- [ ] Markdown syntax highlighting
- [ ] Auto-detect markdown
- [ ] Parse button
- [ ] Collapse after parse
- [ ] Implement Open tab
- [ ] Support .md files
- [ ] Support .txt files
- [ ] Support .html files (convert to markdown)
- [ ] Support .docx files (convert to markdown)
- [ ] Auto-load on file selection
#### Editor Section
- [ ] Set up CodeMirror for markdown
- [ ] Install @codemirror/lang-markdown
- [ ] Configure markdown mode
- [ ] Add syntax highlighting
- [ ] Add line numbers
- [ ] Add line wrapping
- [ ] Implement split view layout
- [ ] Editor pane (left)
- [ ] Preview pane (right)
- [ ] Resizable divider (optional)
- [ ] Implement view mode toggle
- [ ] Split view (default)
- [ ] Editor only
- [ ] Preview only
- [ ] Fullscreen mode
- [ ] Add markdown toolbar
- [ ] Bold button (Ctrl+B)
- [ ] Italic button (Ctrl+I)
- [ ] H1 button
- [ ] H2 button
- [ ] H3 button
- [ ] Link button (Ctrl+K)
- [ ] Image button
- [ ] Code button (Ctrl+`)
- [ ] Quote button
- [ ] Unordered list button
- [ ] Ordered list button
- [ ] Table button
- [ ] Add editor features
- [ ] Word count
- [ ] Character count
- [ ] Line count
- [ ] Reading time estimate
#### Preview Section
- [ ] Set up markdown parser (marked)
- [ ] Install marked
- [ ] Install DOMPurify
- [ ] Configure marked options
- [ ] Implement live preview
- [ ] Real-time rendering
- [ ] Debounce for performance
- [ ] Scroll sync (optional)
- [ ] Add syntax highlighting for code blocks
- [ ] Install highlight.js
- [ ] Configure languages
- [ ] Apply highlighting
- [ ] Add GitHub Flavored Markdown support
- [ ] Tables
- [ ] Strikethrough
- [ ] Task lists
- [ ] Autolinks
- [ ] Implement Table of Contents
- [ ] Auto-generate from headers
- [ ] Clickable links
- [ ] Collapsible (optional)
- [ ] Add mermaid diagram rendering
- [ ] Install mermaid
- [ ] Detect mermaid code blocks
- [ ] Render diagrams
- [ ] Error handling
#### Export Section
- [ ] Create collapsible export section
- [ ] Implement Markdown export
- [ ] Standard Markdown
- [ ] GitHub Flavored Markdown
- [ ] CommonMark
- [ ] Copy to clipboard
- [ ] Download as .md file
- [ ] Implement HTML export
- [ ] Standalone HTML with CSS
- [ ] Inline styles
- [ ] Include syntax highlighting CSS
- [ ] Copy to clipboard
- [ ] Download as .html file
- [ ] Implement Plain Text export
- [ ] Strip all formatting
- [ ] Copy to clipboard
- [ ] Download as .txt file
- [ ] Implement PDF export
- [ ] Install html2pdf.js
- [ ] Convert HTML to PDF
- [ ] Maintain formatting
- [ ] Download as .pdf file
- [ ] Implement DOCX export
- [ ] Install docx library
- [ ] Convert markdown to DOCX
- [ ] Maintain formatting
- [ ] Download as .docx file
#### Conversion Features
- [ ] HTML to Markdown conversion
- [ ] Install turndown
- [ ] Convert on paste (if HTML detected)
- [ ] Convert on file open (.html)
- [ ] DOCX to Markdown conversion
- [ ] Install mammoth.js
- [ ] Convert on file open (.docx)
- [ ] Extract text and formatting
#### Usage Tips
- [ ] Create collapsible Usage Tips section
- [ ] Add Input Methods tips
- [ ] Add Editor Features tips
- [ ] Add Markdown Syntax tips
- [ ] Add Export Options tips
- [ ] Add Data Privacy tips
#### Data Loss Prevention
- [ ] Implement hasUserData() function
- [ ] Implement hasModifiedData() function
- [ ] Add confirmation modal for tab changes
- [ ] Add confirmation for Create New buttons
#### Testing
- [ ] Test all input methods
- [ ] Test markdown rendering
- [ ] Test all export formats
- [ ] Test HTML to Markdown conversion
- [ ] Test DOCX import
- [ ] Test mermaid diagrams
- [ ] Test code syntax highlighting
- [ ] Test Table of Contents
- [ ] Test view mode toggle
- [ ] Test toolbar buttons
- [ ] Test keyboard shortcuts
- [ ] Test responsive design
- [ ] Test dark mode
- [ ] Test on mobile devices
#### Documentation
- [ ] Add to EDITOR_TOOL_GUIDE.md
- [ ] Create user guide
- [ ] Add screenshots
- [ ] Create tutorial video (optional)
---
### 📝 Markdown Editor - Post-MVP (Future)
#### Advanced Markdown Features
- [ ] Add table support (GitHub-style)
- [ ] Add task lists (checkboxes)
- [ ] Add footnotes support
- [ ] Add emoji support (:smile:)
- [ ] Add math equations (KaTeX)
- [ ] Install katex
- [ ] Detect math blocks
- [ ] Render equations
#### Templates
- [ ] Create README.md template
- [ ] Create Documentation template
- [ ] Create Blog post template
- [ ] Create Meeting notes template
- [ ] Create Project proposal template
- [ ] Add template selector UI
- [ ] Allow custom templates
#### Utilities
- [ ] Add markdown linter
- [ ] Check for common issues
- [ ] Suggest improvements
- [ ] Show warnings
- [ ] Add link checker
- [ ] Validate URLs
- [ ] Check for broken links
- [ ] Show status
- [ ] Add format beautifier
- [ ] Clean up markdown
- [ ] Consistent formatting
- [ ] Fix indentation
- [ ] Add image optimizer
- [ ] Compress images
- [ ] Convert to base64
- [ ] Optimize for web
#### Enhanced Features
- [ ] Add keyboard shortcuts
- [ ] Add auto-save (localStorage)
- [ ] Add export history
- [ ] Add version history
- [ ] Add collaborative editing (future)
---
### 🎨 Diagram Tool - MVP (1-2 weeks)
#### Planning
- [ ] Finalize feature list for MVP
- [ ] Design UI mockup
- [ ] Plan component structure
- [ ] Research mermaid.js capabilities
#### Project Setup
- [ ] Create `DiagramEditor.jsx` page
- [ ] Set up routing (`/diagram-editor`)
- [ ] Add to navigation menu
- [ ] Add to homepage tools list
#### Input Section
- [ ] Implement Create New tab
- [ ] Start Empty button
- [ ] Load Sample button (with example diagrams)
- [ ] Diagram type selector
- [ ] Implement URL tab
- [ ] Use AdvancedURLFetch component
- [ ] Support mermaid code URLs
- [ ] Support PlantUML URLs (future)
- [ ] Implement Paste tab
- [ ] CodeMirror editor
- [ ] Mermaid syntax highlighting
- [ ] Parse button
- [ ] Collapse after parse
- [ ] Implement Open tab
- [ ] Support .mmd files
- [ ] Support .mermaid files
- [ ] Support .txt files
- [ ] Auto-load on file selection
#### Editor Section
- [ ] Set up CodeMirror for mermaid
- [ ] Configure mermaid mode
- [ ] Add syntax highlighting
- [ ] Add line numbers
- [ ] Implement diagram type selector
- [ ] Flowchart
- [ ] Sequence Diagram
- [ ] Class Diagram
- [ ] State Diagram
- [ ] Entity Relationship Diagram
- [ ] Gantt Chart
- [ ] Pie Chart
- [ ] Git Graph
- [ ] Add code templates for each diagram type
- [ ] Add error detection
- [ ] Syntax errors
- [ ] Invalid diagram type
- [ ] Show error messages
#### Preview Section
- [ ] Set up mermaid.js
- [ ] Install mermaid
- [ ] Configure mermaid
- [ ] Initialize mermaid
- [ ] Implement live preview
- [ ] Real-time rendering
- [ ] Debounce for performance
- [ ] Error handling
- [ ] Add zoom controls
- [ ] Zoom in button
- [ ] Zoom out button
- [ ] Reset zoom button
- [ ] Fit to screen button
- [ ] Add pan controls
- [ ] Drag to pan
- [ ] Pan with mouse wheel
- [ ] Add fullscreen mode
#### Export Section
- [ ] Create collapsible export section
- [ ] Implement PNG export
- [ ] Install html2canvas
- [ ] Convert SVG to PNG
- [ ] Download as .png file
- [ ] Implement SVG export
- [ ] Extract SVG from preview
- [ ] Download as .svg file
- [ ] Implement PDF export
- [ ] Convert SVG to PDF
- [ ] Download as .pdf file
- [ ] Implement Mermaid Code export
- [ ] Copy to clipboard
- [ ] Download as .mmd file
#### Usage Tips
- [ ] Create collapsible Usage Tips section
- [ ] Add Input Methods tips
- [ ] Add Diagram Types tips
- [ ] Add Mermaid Syntax tips
- [ ] Add Export Options tips
- [ ] Add Data Privacy tips
#### Data Loss Prevention
- [ ] Implement hasUserData() function
- [ ] Implement hasModifiedData() function
- [ ] Add confirmation modal for tab changes
- [ ] Add confirmation for Create New buttons
#### Testing
- [ ] Test all input methods
- [ ] Test all diagram types
- [ ] Test diagram rendering
- [ ] Test all export formats
- [ ] Test zoom controls
- [ ] Test pan controls
- [ ] Test fullscreen mode
- [ ] Test error handling
- [ ] Test responsive design
- [ ] Test dark mode
- [ ] Test on mobile devices
#### Documentation
- [ ] Add to EDITOR_TOOL_GUIDE.md
- [ ] Create user guide
- [ ] Add screenshots
- [ ] Create tutorial video (optional)
---
### 🎨 Diagram Tool - Post-MVP (Future)
#### Visual Editor
- [ ] Design drag-and-drop interface
- [ ] Implement node library (shapes, icons)
- [ ] Implement connection tools
- [ ] Implement styling options (colors, fonts, borders)
- [ ] Add Excalidraw-style drawing
- [ ] Add auto-layout options
#### Advanced Features
- [ ] Add PlantUML import
- [ ] Add custom themes
- [ ] Add diagram templates
- [ ] Add collaboration features (future)
- [ ] Add version history
---
## 📋 Phase 3: Monetization Backend
### 💳 Payment Integration (1 week)
#### Planning
- [ ] Choose payment provider (Stripe recommended)
- [ ] Plan payment flow
- [ ] Plan database schema
- [ ] Plan backend architecture
#### Stripe Setup
- [ ] Create Stripe account
- [ ] Verify business information
- [ ] Set up pricing plans in Stripe
- [ ] 1 month: $2.99
- [ ] 3 months: $6.99
- [ ] 6 months: $11.99
- [ ] 12 months: $19.99
- [ ] Get API keys (test and live)
#### Backend Setup
- [ ] Choose backend (Node.js + Express or serverless)
- [ ] Set up backend project
- [ ] Set up database (PostgreSQL or MongoDB)
- [ ] Create database schema
- [ ] Users table
- [ ] Purchases table
- [ ] Ad-free status table
- [ ] Set up environment variables
#### API Development
- [ ] Create payment intent endpoint
- [ ] Create payment confirmation endpoint
- [ ] Create purchase history endpoint
- [ ] Create ad-free status endpoint
- [ ] Add error handling
- [ ] Add logging
#### Frontend Integration
- [ ] Design payment flow UI
- [ ] Create checkout page
- [ ] Integrate Stripe Elements
- [ ] Implement payment form
- [ ] Implement payment success handling
- [ ] Implement payment failure handling
- [ ] Add loading states
- [ ] Add error messages
#### Email Integration
- [ ] Choose email service (SendGrid recommended)
- [ ] Set up email service account
- [ ] Create email templates
- [ ] Purchase confirmation
- [ ] Receipt
- [ ] Expiry reminder
- [ ] Implement email sending
#### Testing
- [ ] Test payment flow (test mode)
- [ ] Test with test cards
- [ ] Test payment success
- [ ] Test payment failure
- [ ] Test email sending
- [ ] Test with real payment (small amount)
- [ ] Test on different browsers
- [ ] Test on mobile devices
#### Deployment
- [ ] Deploy backend
- [ ] Set up production database
- [ ] Configure environment variables
- [ ] Test in production
- [ ] Monitor transactions
#### Legal & Compliance
- [ ] Add refund policy
- [ ] Add customer support email
- [ ] Update Terms of Service
- [ ] Update Privacy Policy
---
### 👤 User Accounts (1 week)
#### Planning
- [ ] Choose auth provider (Firebase Auth or Auth0)
- [ ] Plan auth flow
- [ ] Plan user data structure
#### Auth Setup
- [ ] Create auth provider account
- [ ] Configure auth provider
- [ ] Get API keys
- [ ] Set up authentication methods
- [ ] Email + password
- [ ] Google OAuth (optional)
- [ ] GitHub OAuth (optional)
#### UI Development
- [ ] Design login page
- [ ] Design signup page
- [ ] Design profile page
- [ ] Create login form
- [ ] Create signup form
- [ ] Create profile page
- [ ] Add form validation
#### Auth Implementation
- [ ] Implement email + password auth
- [ ] Implement OAuth (optional)
- [ ] Implement logout functionality
- [ ] Implement password reset flow
- [ ] Implement email verification
- [ ] Add session management
#### User Profile
- [ ] Display user information
- [ ] Display ad-free status
- [ ] Display purchase history
- [ ] Add edit profile functionality
- [ ] Add change password functionality
#### Integration
- [ ] Connect auth to payment system
- [ ] Store user ID with purchases
- [ ] Track ad-free status
- [ ] Update UI based on auth state
#### Testing
- [ ] Test signup flow
- [ ] Test login flow
- [ ] Test logout flow
- [ ] Test password reset
- [ ] Test email verification
- [ ] Test on different browsers
- [ ] Test on mobile devices
#### Security
- [ ] Add rate limiting
- [ ] Add CSRF protection
- [ ] Add XSS protection
- [ ] Add SQL injection protection
- [ ] Implement secure session handling
---
### 🚫 Ad-Free Experience (3 days)
#### UI Development
- [ ] Design ad-free badge
- [ ] Design expiry reminder
- [ ] Design renewal flow
- [ ] Create ad-free badge component
- [ ] Create expiry reminder component
#### Implementation
- [ ] Check ad-free status on page load
- [ ] Hide ads for ad-free users
- [ ] Show ad-free badge in UI
- [ ] Implement expiry reminder (7 days before)
- [ ] Implement grace period (3 days after expiry)
- [ ] Add renewal flow
#### Testing
- [ ] Test ad-free status check
- [ ] Test ad hiding
- [ ] Test ad-free badge display
- [ ] Test expiry reminder
- [ ] Test grace period
- [ ] Test renewal flow
- [ ] Test on different browsers
- [ ] Test on mobile devices
#### Monitoring
- [ ] Track ad-free user count
- [ ] Monitor expiry dates
- [ ] Send expiry reminders
- [ ] Collect feedback from ad-free users
---
## 🎯 Quick Wins (Can Do Anytime)
### SEO & Marketing
- [ ] Optimize page titles
- [ ] Optimize meta descriptions
- [ ] Add Open Graph tags
- [ ] Add Twitter Card tags
- [ ] Create sitemap.xml
- [ ] Submit to Google Search Console
- [ ] Create robots.txt
### Content
- [ ] Write blog post: "How to use Object Editor"
- [ ] Write blog post: "How to use Table Editor"
- [ ] Write blog post: "How to use Invoice Editor"
- [ ] Create tutorial videos
- [ ] Share on Reddit
- [ ] Share on Hacker News
- [ ] Share on Twitter/X
### UX Improvements
- [ ] Add loading skeletons
- [ ] Add empty states
- [ ] Add success messages
- [ ] Add error messages
- [ ] Improve error handling
- [ ] Add tooltips
- [ ] Add keyboard shortcuts
### Performance
- [ ] Implement code splitting
- [ ] Lazy load components
- [ ] Optimize images
- [ ] Minify CSS/JS
- [ ] Add caching headers
- [ ] Implement service worker (PWA)
---
## 📊 Metrics to Track
### User Metrics
- [ ] Daily active users (DAU)
- [ ] Weekly active users (WAU)
- [ ] Monthly active users (MAU)
- [ ] New users per day
- [ ] Returning users
- [ ] User retention rate
### Tool Usage
- [ ] Object Editor usage
- [ ] Table Editor usage
- [ ] Invoice Editor usage
- [ ] Markdown Editor usage (when launched)
- [ ] Diagram Tool usage (when launched)
- [ ] Most popular features
### Monetization
- [ ] Ad impressions per day
- [ ] Ad clicks per day
- [ ] Ad CTR (Click-Through Rate)
- [ ] Ad revenue per day
- [ ] Ad-free purchases per month
- [ ] Ad-free revenue per month
- [ ] Total revenue per month
### Performance
- [ ] Page load time
- [ ] Time to interactive
- [ ] First contentful paint
- [ ] Largest contentful paint
- [ ] Cumulative layout shift
---
**End of To-Do List**

2
nixpacks.toml Normal file
View File

@@ -0,0 +1,2 @@
[phases.build]
cmd = "npm run build:no-snap"

2050
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,30 +4,49 @@
"description": "Web Developer Tools MVP - Utilities Toolkit",
"private": true,
"dependencies": {
"@codemirror/basic-setup": "^0.20.0",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.4.0",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.1",
"@codemirror/view": "^6.38.3",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@uiw/react-codemirror": "^4.25.1",
"codemirror": "^6.0.2",
"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",
"js-beautify": "^1.15.4",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"lucide-react": "^0.540.0",
"marked": "^16.4.1",
"marked-emoji": "^2.0.1",
"papaparse": "^5.5.3",
"react": "18.3.1",
"react-diff-view": "^3.3.2",
"react-dom": "18.3.1",
"react-helmet-async": "^2.0.5",
"react-router-dom": "6.26.2",
"react-scripts": "5.0.1",
"react-snap": "^1.23.0",
"reactflow": "^11.11.4",
"serialize-javascript": "^6.0.0",
"serve": "^14.2.4",
"turndown": "^7.2.1",
"web-vitals": "^2.1.4"
},
"devDependencies": {
@@ -38,10 +57,37 @@
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"start:prod": "serve -s build -l 3000",
"build": "react-scripts build && react-snap",
"build:no-snap": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"reactSnap": {
"inlineCss": true,
"minifyHtml": {
"collapseWhitespace": false,
"removeComments": false
},
"puppeteerArgs": [
"--no-sandbox",
"--disable-setuid-sandbox"
],
"include": [
"/",
"/object-editor",
"/table-editor",
"/invoice-editor",
"/url",
"/base64",
"/beautifier",
"/diff",
"/text-length",
"/whats-new",
"/privacy",
"/terms"
]
},
"eslintConfig": {
"extends": [
"react-app",

1
public/ads.txt Normal file
View File

@@ -0,0 +1 @@
google.com, pub-8644544686212757, DIRECT, f08c47fec0942fa0

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

253
public/data/commits.json Normal file
View File

@@ -0,0 +1,253 @@
{
"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",
"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",
"type": "enhancement",
"title": "Object Editor Preview Mode",
"description": "Added Preview/Edit mode toggle to Object Editor's Tree View. Preview mode shows data in read-only format with full text visibility - perfect for viewing long values without editing. Click any nested JSON/serialized value directly in preview mode to explore its structure in a modal, no need to switch to edit mode first. Also improved mobile layout for data load notices and table editor."
}
]
},
{
"date": "2025-10-14",
"changes": [
{
"datetime": "2025-10-14T23:59:00+07:00",
"type": "enhancement",
"title": "Editor UX Refinement - Collapsible Sections & Usage Tips",
"description": "Enhanced all editor tools (Object, Table, Invoice) with collapsible export sections and comprehensive usage tips. Export sections now start collapsed to reduce page height, and usage tips are collapsible with eye-catching design. Added detailed documentation guides (EDITOR_TOOL_GUIDE.md, EDITOR_CHECKLIST.md) for consistent future development."
},
{
"datetime": "2025-10-14T22:30:00+07:00",
"type": "enhancement",
"title": "Consistent Input Methods Across All Editors",
"description": "Standardized file input and URL fetch UI across Object, Table, and Invoice editors. File inputs now use consistent 'tool-input' class and auto-load content immediately. URL inputs feature inline fetch buttons (not below) for better UX. Paste sections now collapse after successful parsing with data summaries."
},
{
"datetime": "2025-10-14T21:00:00+07:00",
"type": "enhancement",
"title": "Data Loss Prevention System",
"description": "Implemented comprehensive confirmation modals across all editors to prevent accidental data loss. When switching input methods or tabs with existing data, users now see a detailed warning with specific data summary and option to save before proceeding. Features amber-themed warning design with clear action buttons."
},
{
"datetime": "2025-10-14T20:00:00+07:00",
"type": "fix",
"title": "Table Editor Cell Overflow Fix",
"description": "Fixed text overflow issues in Table Editor cells. Long text now properly truncates with ellipsis instead of breaking layout. Added proper overflow handling with 'truncate' class and block-level spans for consistent cell rendering."
},
{
"datetime": "2025-10-14T19:30:00+07:00",
"type": "enhancement",
"title": "Object Editor Output Section Improvements",
"description": "Made Object Editor's output/export section collapsible to reduce page height when working with long data. Export section now starts collapsed with a clickable header showing data summary, matching the pattern used in other editors."
},
{
"datetime": "2025-10-14T19:00:00+07:00",
"type": "fix",
"title": "Object Editor File Input Improvements",
"description": "Fixed Object Editor's 'Open File' tab to auto-load content immediately upon file selection, matching Table and Invoice editor behavior. Removed unnecessary 'Parse Object' button and irrelevant CSV checkbox for cleaner, more intuitive UX."
},
{
"datetime": "2025-10-14T18:30:00+07:00",
"type": "enhancement",
"title": "Invoice Editor Export & URL Consistency",
"description": "Made Invoice Editor's export section collapsible to match other editors. Fixed URL form layout to use inline fetch button instead of below-input placement. Improved overall consistency across all editor input methods."
}
]
},
{
"date": "2025-09-28",
"changes": [
{
"datetime": "2025-09-28T23:26:38+07:00",
"type": "enhancement",
"title": "UI Consistency & Code Quality Improvements",
"description": "Standardized InvoiceEditor CreateNew tab styling to match ObjectEditor design, fixed CodeMirror focus issues during editing, removed copy button from mindmap view, resolved ESLint warnings, and improved overall code quality."
},
{
"datetime": "2025-09-28T20:11:39+07:00",
"type": "enhancement",
"title": "Invoice Settings & UI Improvements",
"description": "Reorganized invoice settings modal: renamed 'Payment Methods' to 'Payment', moved payment status controls to Payment tab, added decimal digits setting (0-3), enhanced subtotal/fees/discounts with consistent padding and background styling for better visual hierarchy."
},
{
"datetime": "2025-09-28T18:49:30+07:00",
"type": "enhancement",
"title": "Enhanced Invoice Payment Status System",
"description": "Improved payment status stamps with better positioning after total, wider subtotal card for Indonesian currency formats, added 'Partially Paid' option, payment date tracking for PAID invoices, and 'Paid at:' display in payment methods."
},
{
"datetime": "2025-09-28T17:43:49+07:00",
"type": "fix",
"title": "Invoice Preview & UI Improvements",
"description": "Fixed subtotal alignment in invoice preview, added conditional rendering for empty address fields, implemented payment status stamps, and resolved React key warnings in release notes."
},
{
"datetime": "2025-09-28T17:11:19+07:00",
"type": "enhancement",
"title": "Release Notes System Improvements",
"description": "Updated release notes to use new JSON structure with individual commit timestamps, removed hash display, and moved data to /public/data/ for better organization."
},
{
"datetime": "2025-09-28T17:10:30+07:00",
"type": "fix",
"title": "Invoice Installment Calculation Fix",
"description": "Fixed automatic recalculation of percentage-based installments in both Invoice Editor and Preview when invoice totals change due to item, fee, or discount modifications."
},
{
"datetime": "2025-09-28T17:09:45+07:00",
"type": "enhancement",
"title": "Logo Integration & Layout Improvements",
"description": "Integrated custom logo.svg in header and footer, removed background styling for cleaner logo display, and cleaned up unused release data files."
},
{
"datetime": "2025-09-28T00:41:48+07:00",
"type": "feature",
"title": "SEO & Privacy Compliance",
"description": "Comprehensive SEO optimization with GDPR-compliant analytics and consent management."
}
]
},
{
"date": "2025-09-27",
"changes": [
{
"datetime": "2025-09-27T23:54:19+07:00",
"type": "feature",
"title": "Mobile UI Improvements",
"description": "Optimized interface for mobile devices with better analytics integration."
},
{
"datetime": "2025-09-27T23:14:26+07:00",
"type": "enhancement",
"title": "Enhanced Object Editor & Table View",
"description": "Improved user interface and experience with better JSON parsing, HTML rendering, and copy functionality."
},
{
"datetime": "2025-09-27T22:20:13+07:00",
"type": "feature",
"title": "What's New Feature & Navigation Improvements",
"description": "Added attractive 'What's New' button to homepage, created NON_TOOLS category for better navigation organization, and implemented unified global footer across all pages."
},
{
"datetime": "2025-09-27T21:28:43+07:00",
"type": "feature",
"title": "Invoice Editor Major Update",
"description": "Complete overhaul of Invoice Editor with currency system, PDF generation fixes, improved UI/UX, and streamlined preview toolbar."
},
{
"datetime": "2025-09-27T20:25:56+07:00",
"type": "feature",
"title": "Enhanced Workflow for All Tools",
"description": "Added convenient 'Clear', 'Copy', 'Sample', and 'Download' buttons across all tools to help streamline your workflow."
},
{
"datetime": "2025-09-27T19:54:41+07:00",
"type": "fix",
"title": "General Bug Fixes",
"description": "Addressed various minor bugs and improved overall site performance for a faster, smoother experience."
}
]
},
{
"date": "2025-09-21",
"changes": [
{
"datetime": "2025-09-21T17:29:46+07:00",
"type": "enhancement",
"title": "Improved Tool Pages",
"description": "Every tool now features a clear header, breadcrumb navigation, and a helpful description to make finding and using them easier than ever."
},
{
"datetime": "2025-09-21T16:51:17+07:00",
"type": "feature",
"title": "New 'What's New' Page",
"description": "Launched this 'What's New' page to keep you updated on the latest changes."
}
]
},
{
"date": "2025-09-19",
"changes": [
{
"datetime": "2025-09-19T00:09:05+07:00",
"type": "fix",
"title": "UI Fixes",
"description": "Corrected a UI bug in the Text Extractor tool and improved the copy button functionality."
}
]
},
{
"date": "2025-09-18",
"changes": [
{
"datetime": "2025-09-18T23:44:39+07:00",
"type": "enhancement",
"title": "Sidebar and Menu Enhancements",
"description": "Improved the tool sidebar and mobile menu for better navigation."
}
]
},
{
"date": "2025-08-21",
"changes": [
{
"datetime": "2025-08-21T23:45:46+07:00",
"type": "feature",
"title": "New Tools Added",
"description": "A comprehensive suite of developer tools has been added to the site, ready for you to use."
},
{
"datetime": "2025-08-21T23:17:54+07:00",
"type": "feature",
"title": "Website Launch",
"description": "Welcome to our new site! We're excited to launch with a full set of developer tools."
}
]
}
]
}

File diff suppressed because one or more lines are too long

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/icon-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/icon-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -3,15 +3,23 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="%PUBLIC_URL%/android-chrome-192x192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="%PUBLIC_URL%/android-chrome-512x512.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0ea5e9" />
<meta name="description" content="Developer Tools MVP - Essential utilities for web developers" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Developer Tools - Web Developer Utilities</title>
<!-- Google AdSense -->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8644544686212757"
crossorigin="anonymous"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
</html>

72
public/logo.svg Normal file
View File

@@ -0,0 +1,72 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 75" width="300" height="75">
<defs>
<radialGradient id="g1" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(44.318,10.028,-10.028,44.318,53.669,12.735)">
<stop offset="0" stop-color="#d056a5"/>
<stop offset="1" stop-color="#6665e9"/>
</radialGradient>
<clipPath clipPathUnits="userSpaceOnUse" id="cp1">
<path d="m32.56 20.66c0.59-2.02 2.46-3.4 4.57-3.41 2.07 0.01 4.25 0.01 4.25 0.01 0 0-8.18 27.43-11.08 37.2-0.6 2.03-2.47 3.41-4.59 3.42-2.05 0-4.25 0-4.25 0 0 0 8.18-27.43 11.1-37.22z"/>
</clipPath>
<linearGradient id="g2" x2="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-8.104,25.657,-4.604,-1.454,34.912,29.585)">
<stop offset="0" stop-color="#d056a5" stop-opacity=".21"/>
<stop offset="1" stop-color="#d056a5" stop-opacity="0"/>
</linearGradient>
</defs>
<style>
.s0 { fill: #4a5567 }
.s1 { fill: #808080 }
.s2 { fill: url(#g1) }
.s3 { fill: #ffffff }
.s4 { fill: url(#g2) }
</style>
<g>
<g id="d">
<path class="s0" d="m105.66 24.68c-3.24-3.13-7.33-4.81-11.77-4.81-10.34 0-18.51 8.05-18.51 19.47 0 11.18 8.17 19.35 18.51 19.35 4.44 0 8.65-1.68 11.77-4.93l0.72 4.33h8.54v-49.87h-9.26zm0 14.66c0 6.37-4.8 10.45-10.81 10.45-5.77 0-10.46-4.08-10.46-10.45 0-6.49 4.69-10.58 10.46-10.58 6.01 0 10.81 4.09 10.81 10.58z"/>
</g>
<g id="e">
<path class="s0" d="m146.16 44.51c-1.56 3.24-5.17 5.17-9.01 5.17-5.05 0-9.25-3.13-10.1-8.18h29.33v-3.48c-0.36-11.18-7.93-18.15-19.23-18.15-10.82 0-19.59 8.05-19.59 19.47 0 11.18 8.77 19.35 19.59 19.35 8.89 0 17.54-5.77 18.87-14.18zm-18.27-10.34c1.8-3.24 5.17-5.28 9.25-5.28 3.97 0 7.09 2.04 8.66 5.28z"/>
</g>
<g id="w">
<path class="s0" d="m200.48 58.09l13.7-37.86h-9.61l-7.93 23.2-8.06-23.2h-8.05l-7.93 23.2-7.93-23.2h-9.74l13.82 37.86h7.7l8.05-22.47 8.17 22.47z"/>
</g>
<g id="e.2">
<path class="s0" d="m240.86 44.51c-1.68 3.24-5.17 5.17-9.01 5.17-5.05 0-9.26-3.13-10.22-8.18h29.44v-3.48c-0.36-11.18-8.05-18.15-19.22-18.15-10.82 0-19.59 8.05-19.59 19.47 0 11.18 8.77 19.35 19.59 19.35 8.89 0 17.54-5.77 18.86-14.18zm-18.26-10.34c1.68-3.24 5.16-5.28 9.25-5.28 3.85 0 7.09 2.04 8.65 5.28z"/>
</g>
<g id=".">
<path class="s1" d="m252.55 58.21q-0.65 0.01-1.1-0.44-0.46-0.46-0.46-1.1 0-0.66 0.46-1.11 0.45-0.46 1.1-0.46 0.63 0 1.07 0.46 0.44 0.45 0.44 1.11 0 0.64-0.44 1.1-0.44 0.45-1.07 0.44z"/>
</g>
<g id="d">
<path class="s1" d="m255.74 51.14q0-2.11 0.85-3.71 0.84-1.59 2.35-2.47 1.49-0.87 3.35-0.86 1.6 0 2.99 0.72 1.35 0.75 2.11 1.95v-7.27h2.31v18.57h-2.31v-2.59q-0.69 1.23-2.01 2.03-1.35 0.78-3.11 0.78-1.83 0-3.33-0.9-1.51-0.91-2.35-2.53-0.85-1.63-0.85-3.72zm11.65 0.03q-0.01-1.57-0.65-2.71-0.63-1.16-1.68-1.77-1.06-0.63-2.35-0.62-1.27 0-2.33 0.6-1.05 0.6-1.68 1.77-0.63 1.14-0.63 2.7 0 1.58 0.63 2.76 0.63 1.15 1.68 1.76 1.06 0.63 2.33 0.62 1.29 0.01 2.35-0.62 1.05-0.61 1.68-1.76 0.64-1.17 0.65-2.73z"/>
</g>
<g id="e">
<path class="s1" d="m285.59 50.66q0.01 0.67-0.08 1.39h-10.98q0.12 2.02 1.39 3.17 1.26 1.14 3.07 1.14c0.98 0 1.83-0.24 2.47-0.7q0.99-0.69 1.4-1.83h2.45q-0.54 1.97-2.21 3.22-1.65 1.24-4.11 1.24-1.96 0-3.49-0.88-1.55-0.87-2.43-2.49-0.87-1.62-0.87-3.75 0-2.14 0.85-3.74 0.85-1.62 2.39-2.47 1.56-0.87 3.55-0.86 1.96 0 3.47 0.84 1.5 0.85 2.31 2.35c0.56 0.98 0.82 2.13 0.82 3.37zm-2.34-0.48q-0.01-1.29-0.59-2.25-0.57-0.94-1.56-1.42-1-0.48-2.21-0.48-1.72-0.01-2.95 1.1-1.21 1.09-1.39 3.05z"/>
</g>
<g id="v">
<path class="s1" d="m293.22 55.96l4.28-11.64h2.43l-5.4 13.75h-2.65l-5.4-13.75h2.45z"/>
</g>
<g id="Folder 1">
<g>
<g>
<path fill-rule="evenodd" class="s2" d="m62.85 21.84v31.43c0 8.66-7.04 15.71-15.71 15.71h-31.43c-8.68 0-15.72-7.05-15.72-15.71v-31.43c0-8.68 7.04-15.72 15.72-15.72h31.43c8.67 0 15.71 7.04 15.71 15.72z"/>
</g>
</g>
<g>
<g>
<path fill-rule="evenodd" class="s3" d="m21.02 52.24l-7.88-6.43c-2.51-2.05-3.96-5.13-3.95-8.37 0-3.25 1.49-6.31 4-8.33l6.67-5.37c1.92-1.54 4.73-1.23 6.28 0.68l1.9 2.35-10.14 8.16c-0.77 0.62-1.23 1.55-1.23 2.54 0 0.99 0.44 1.91 1.19 2.53l5.46 4.47zm20.43-28.1l8.2 6.59c2.53 2.04 4 5.1 4.02 8.34 0.01 3.25-1.44 6.32-3.96 8.36l-6.72 5.5c-1.92 1.56-4.72 1.27-6.29-0.65l-1.91-2.33 10.19-8.31c0.77-0.62 1.21-1.56 1.21-2.54-0.02-0.99-0.46-1.91-1.23-2.53l-5.81-4.68zm-14.7 23.11l1.31 1.07-2.55 3.12zm8.95-18.14l-0.88-0.71 1.74-2.2z"/>
</g>
</g>
<g>
<g>
<path fill-rule="evenodd" class="s3" d="m32.56 20.66c0.59-2.02 2.46-3.4 4.57-3.41 2.07 0.01 4.25 0.01 4.25 0.01 0 0-8.18 27.43-11.08 37.2-0.6 2.03-2.47 3.41-4.59 3.42-2.05 0-4.25 0-4.25 0 0 0 8.18-27.43 11.1-37.22z"/>
<g id="Clip-Path" clip-path="url(#cp1)">
<g>
<g>
<path fill-rule="evenodd" class="s4" d="m36.04 20.66c0.59-2.02 2.46-3.4 4.57-3.41 2.07 0.01 4.25 0.01 4.25 0.01 0 0-8.18 27.43-11.08 37.2-0.6 2.03-2.47 3.41-4.58 3.42-2.06 0-4.25 0-4.25 0 0 0 8.18-27.43 11.09-37.22z"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

29
public/robots.txt Normal file
View File

@@ -0,0 +1,29 @@
# Robots.txt for https://dewe.dev
# Generated automatically
User-agent: *
Allow: /
# Sitemap location
Sitemap: https://dewe.dev/sitemap.xml
# Block any future admin or private routes
Disallow: /admin/
Disallow: /api/
Disallow: /.well-known/
# Allow all major search engines
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Slurp
Allow: /
User-agent: DuckDuckBot
Allow: /
# Crawl delay for politeness
Crawl-delay: 1

101
public/sitemap.xml Normal file
View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<!-- Homepage -->
<url>
<loc>https://dewe.dev/</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<!-- Editor Tools (High Priority) -->
<url>
<loc>https://dewe.dev/object-editor</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dewe.dev/table-editor</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://dewe.dev/invoice-editor</loc>
<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>
<priority>0.9</priority>
</url>
<!-- Encoder Tools -->
<url>
<loc>https://dewe.dev/url</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/base64</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/beautifier</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<!-- Utility Tools -->
<url>
<loc>https://dewe.dev/diff</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://dewe.dev/text-length</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<!-- Info Pages -->
<url>
<loc>https://dewe.dev/whats-new</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://dewe.dev/release-notes</loc>
<lastmod>2025-10-22</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<!-- Legal Pages -->
<url>
<loc>https://dewe.dev/privacy</loc>
<lastmod>2025-10-15</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://dewe.dev/terms</loc>
<lastmod>2025-10-15</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
</urlset>

View File

@@ -1,38 +1,62 @@
import React from 'react';
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import Layout from './components/Layout';
import ErrorBoundary from './components/ErrorBoundary';
import Home from './pages/Home';
import JsonTool from './pages/JsonTool';
import SerializeTool from './pages/SerializeTool';
import UrlTool from './pages/UrlTool';
import Base64Tool from './pages/Base64Tool';
import CsvJsonTool from './pages/CsvJsonTool';
import BeautifierTool from './pages/BeautifierTool';
import DiffTool from './pages/DiffTool';
import TextLengthTool from './pages/TextLengthTool';
import ObjectEditor from './pages/ObjectEditor';
import TableEditor from './pages/TableEditor';
import InvoiceEditor from './pages/InvoiceEditor';
import MarkdownEditor from './pages/MarkdownEditor';
import InvoicePreview from './pages/InvoicePreview';
import InvoicePreviewMinimal from './pages/InvoicePreviewMinimal';
import ReleaseNotes from './pages/ReleaseNotes';
import TermsOfService from './pages/TermsOfService';
import PrivacyPolicy from './pages/PrivacyPolicy';
import NotFound from './pages/NotFound';
import { initGA } from './utils/analytics';
import './index.css';
function App() {
return (
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/json" element={<JsonTool />} />
<Route path="/serialize" element={<SerializeTool />} />
<Route path="/url" element={<UrlTool />} />
<Route path="/base64" element={<Base64Tool />} />
<Route path="/csv-json" element={<CsvJsonTool />} />
<Route path="/beautifier" element={<BeautifierTool />} />
<Route path="/diff" element={<DiffTool />} />
<Route path="/text-length" element={<TextLengthTool />} />
<Route path="/object-editor" element={<ObjectEditor />} />
// Initialize Google Analytics on app startup
useEffect(() => {
initGA();
}, []);
</Routes>
</Layout>
</Router>
return (
<HelmetProvider>
<ErrorBoundary>
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/url" element={<UrlTool />} />
<Route path="/base64" element={<Base64Tool />} />
<Route path="/beautifier" element={<BeautifierTool />} />
<Route path="/diff" element={<DiffTool />} />
<Route path="/text-length" element={<TextLengthTool />} />
<Route path="/object-editor" element={<ObjectEditor />} />
<Route path="/table-editor" element={<TableEditor />} />
<Route path="/invoice-editor" element={<InvoiceEditor />} />
<Route path="/markdown-editor" element={<MarkdownEditor />} />
<Route path="/invoice-preview" element={<InvoicePreview />} />
<Route path="/invoice-preview-minimal" element={<InvoicePreviewMinimal />} />
<Route path="/whats-new" element={<ReleaseNotes />} />
<Route path="/release-notes" element={<ReleaseNotes />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</Router>
</ErrorBoundary>
</HelmetProvider>
);
}

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

@@ -0,0 +1,99 @@
import React from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView } from '@codemirror/view';
const CodeEditor = ({
value,
onChange,
language = 'json',
placeholder = '',
readOnly = false,
height = '300px',
className = '',
theme = 'light'
}) => {
// Language extensions mapping
const getLanguageExtension = (lang) => {
switch (lang.toLowerCase()) {
case 'javascript':
case 'js':
return [javascript()];
case 'json':
return [json()];
case 'html':
return [html()];
case 'css':
return [css()];
default:
return [json()]; // Default to JSON
}
};
// Theme configuration
const getTheme = () => {
if (theme === 'dark') {
return oneDark;
}
return undefined; // Use default light theme
};
// Extensions
const extensions = [
...getLanguageExtension(language),
EditorView.theme({
'&': {
fontSize: '14px',
},
'.cm-content': {
padding: '16px',
minHeight: height,
},
'.cm-focused': {
outline: 'none',
},
'.cm-editor': {
borderRadius: '8px',
},
'.cm-scroller': {
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
},
}),
EditorView.lineWrapping,
];
return (
<div className={`border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden ${className}`}>
<CodeMirror
value={value}
onChange={onChange}
extensions={extensions}
theme={getTheme()}
placeholder={placeholder}
readOnly={readOnly}
basicSetup={{
lineNumbers: true,
foldGutter: true,
dropCursor: false,
allowMultipleSelections: false,
indentOnInput: true,
bracketMatching: true,
closeBrackets: true,
autocompletion: true,
highlightSelectionMatches: false,
searchKeymap: true,
}}
style={{
fontSize: '14px',
minHeight: height,
}}
/>
</div>
);
};
export default CodeEditor;

View File

@@ -0,0 +1,234 @@
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { EditorView, keymap } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { basicSetup } from 'codemirror';
import { json } from '@codemirror/lang-json';
import { sql } from '@codemirror/lang-sql';
import { markdown } from '@codemirror/lang-markdown';
import { oneDark } from '@codemirror/theme-one-dark';
import { indentWithTab } from '@codemirror/commands';
import { Maximize2, Minimize2 } from 'lucide-react';
const CodeMirrorEditor = ({
value,
onChange,
placeholder = '',
className = '',
language = 'json',
maxLines = 12,
showToggle = true,
cardRef = null, // Reference to the card header for scroll target
height = '350px' // Configurable height, default 350px
}) => {
const editorRef = useRef(null);
const viewRef = useRef(null);
const [isExpanded, setIsExpanded] = useState(false);
const [isDark, setIsDark] = useState(false);
// Check for dark mode
useEffect(() => {
const checkDarkMode = () => {
setIsDark(document.documentElement.classList.contains('dark'));
};
checkDarkMode();
// Watch for dark mode changes
const observer = new MutationObserver(checkDarkMode);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
// Detect if content is single-line (minified)
const isSingleLine = (value || '').split('\n').length === 1;
// Initialize editor only once
useEffect(() => {
if (!editorRef.current || viewRef.current) return;
// Language extension
let langExtension = [];
if (language === 'json') {
langExtension = [json()];
} else if (language === 'sql') {
langExtension = [sql()];
} else if (language === 'markdown') {
langExtension = [markdown()];
}
const extensions = [
basicSetup,
...langExtension,
// Enable Tab key to insert spaces (2 spaces for indentation)
keymap.of([indentWithTab]),
// Enable line wrapping for single-line content OR markdown
...(isSingleLine || language === 'markdown' ? [EditorView.lineWrapping] : []),
EditorView.theme({
'&': {
fontSize: '14px',
width: '100%',
maxWidth: '100%',
},
'.cm-content': {
padding: '12px',
maxWidth: '100%',
},
'.cm-focused': {
outline: 'none',
},
'.cm-editor': {
borderRadius: '6px',
maxWidth: '100%',
},
'.cm-scroller': {
overflowY: 'auto',
overflowX: 'auto',
height: '100%',
maxWidth: '100%',
},
'.cm-line': {
wordBreak: isSingleLine || language === 'markdown' ? 'break-word' : 'normal',
}
}),
EditorView.updateListener.of((update) => {
if (update.docChanged && onChange) {
onChange(update.state.doc.toString());
}
}),
...(isDark ? [oneDark] : [])
].filter(Boolean);
const state = EditorState.create({
doc: value || '',
extensions
});
const view = new EditorView({
state,
parent: editorRef.current
});
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
setTimeout(() => {
const editorElement = editorRef.current?.querySelector('.cm-editor');
const scrollerElement = editorRef.current?.querySelector('.cm-scroller');
if (editorElement) {
editorElement.style.height = height;
editorElement.style.maxHeight = height;
}
if (scrollerElement) {
scrollerElement.style.overflowY = 'auto';
scrollerElement.style.overflowX = isSingleLine ? 'hidden' : 'auto';
scrollerElement.style.height = '100%';
}
// No manual wrapping needed - EditorView.lineWrapping handles it
}, 0);
return () => {
if (viewRef.current) {
viewRef.current.destroy();
viewRef.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDark, isSingleLine, height, language]); // Recreate when theme, line count, height, or language changes
// Apply overflow and height styles
const applyEditorStyles = useCallback(() => {
if (!viewRef.current || !editorRef.current) return;
const editorElement = editorRef.current.querySelector('.cm-editor');
const scrollerElement = editorRef.current.querySelector('.cm-scroller');
if (editorElement) {
if (isExpanded) {
editorElement.style.height = 'auto';
editorElement.style.maxHeight = 'none';
} else {
editorElement.style.height = height;
editorElement.style.maxHeight = height;
}
}
// Handle scrolling
if (scrollerElement) {
scrollerElement.style.overflowY = isExpanded ? 'visible' : 'auto';
scrollerElement.style.height = '100%';
}
}, [isExpanded, height]);
// Apply styles on mount, expand/collapse, and content changes
useEffect(() => {
applyEditorStyles();
}, [applyEditorStyles]);
// Update content when value changes externally
useEffect(() => {
if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
const transaction = viewRef.current.state.update({
changes: {
from: 0,
to: viewRef.current.state.doc.length,
insert: value || ''
}
});
viewRef.current.dispatch(transaction);
// Re-apply styles after content change (e.g., tab switch)
setTimeout(() => applyEditorStyles(), 10);
}
}, [value, applyEditorStyles]);
return (
<div className={`relative ${className}`}>
<div
ref={editorRef}
className={`dewedev-code-mirror border border-gray-300 dark:border-gray-600 rounded-md overflow-hidden ${
isDark ? 'bg-gray-900' : 'bg-white'
} ${isExpanded ? 'h-auto' : `h-[${height}]`}`}
/>
{showToggle && (
<button
onClick={() => {
setIsExpanded(!isExpanded);
setTimeout(() => {
// Scroll to card header if cardRef is provided, otherwise scroll to top
if (cardRef?.current) {
cardRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}, 50);
}}
className="absolute bottom-2 right-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-600 shadow-sm z-10"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? (
<Minimize2 className="h-4 w-4" />
) : (
<Maximize2 className="h-4 w-4" />
)}
</button>
)}
</div>
);
};
export default CodeMirrorEditor;

View File

@@ -0,0 +1,191 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { X, Shield, Settings, Check } from 'lucide-react';
import {
shouldShowConsentBanner,
updateConsent,
CONSENT_CONFIGS,
getConsentBannerData
} from '../utils/consentManager';
const ConsentBanner = () => {
const [isVisible, setIsVisible] = useState(false);
const [showCustomize, setShowCustomize] = useState(false);
const [customConsent, setCustomConsent] = useState({
analytics_storage: false,
ad_storage: false,
ad_personalization: false,
ad_user_data: false
});
const bannerData = getConsentBannerData();
useEffect(() => {
setIsVisible(shouldShowConsentBanner());
}, []);
const handleAcceptAll = () => {
updateConsent(CONSENT_CONFIGS.ACCEPT_ALL);
setIsVisible(false);
};
const handleEssentialOnly = () => {
updateConsent(CONSENT_CONFIGS.ESSENTIAL_ONLY);
setIsVisible(false);
};
const handleCustomSave = () => {
const consentChoices = {
necessary: 'granted',
analytics_storage: customConsent.analytics_storage ? 'granted' : 'denied',
ad_storage: customConsent.ad_storage ? 'granted' : 'denied',
ad_personalization: customConsent.ad_personalization ? 'granted' : 'denied',
ad_user_data: customConsent.ad_user_data ? 'granted' : 'denied'
};
updateConsent(consentChoices);
setIsVisible(false);
};
const toggleCustomConsent = (category) => {
setCustomConsent(prev => ({
...prev,
[category]: !prev[category]
}));
};
if (!isVisible) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-t border-slate-200 dark:border-slate-700 shadow-2xl">
<div className="max-w-7xl mx-auto p-4 sm:p-6">
{!showCustomize ? (
// Main consent banner
<div className="flex flex-col lg:flex-row items-start lg:items-center gap-4">
<div className="flex items-start gap-3 flex-1">
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex-shrink-0">
<Shield className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-slate-800 dark:text-white mb-1">
{bannerData.title}
</h3>
<p className="text-sm text-slate-600 dark:text-slate-300 mb-2">
{bannerData.description}
</p>
<div className="flex flex-wrap gap-2 text-xs">
<Link
to="/privacy"
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 underline"
>
Privacy Policy
</Link>
<span className="text-slate-400"></span>
<Link
to="/terms"
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 underline"
>
Terms of Service
</Link>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 lg:flex-shrink-0 w-full sm:w-auto">
<button
onClick={handleEssentialOnly}
className="px-4 py-3 text-sm font-medium text-slate-600 hover:text-slate-800 dark:text-slate-300 dark:hover:text-white border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
>
Essential Only
</button>
<button
onClick={() => setShowCustomize(true)}
className="px-4 py-3 text-sm font-medium text-slate-600 hover:text-slate-800 dark:text-slate-300 dark:hover:text-white border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors flex items-center justify-center gap-2"
>
<Settings className="h-4 w-4" />
Customize
</button>
<button
onClick={handleAcceptAll}
className="px-4 py-3 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors flex items-center justify-center gap-2"
>
<Check className="h-4 w-4" />
Accept All
</button>
</div>
</div>
) : (
// Customization panel
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-800 dark:text-white">
Customize Cookie Preferences
</h3>
<button
onClick={() => setShowCustomize(false)}
className="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="space-y-4 mb-6">
{bannerData.purposes.map(purpose => (
<div key={purpose.id} className="flex items-start gap-3">
<div className="flex items-center h-5">
{purpose.required ? (
<div className="w-4 h-4 bg-green-500 rounded border flex items-center justify-center">
<Check className="h-3 w-3 text-white" />
</div>
) : (
<input
type="checkbox"
id={purpose.id}
checked={customConsent[purpose.id] || false}
onChange={() => toggleCustomConsent(purpose.id)}
className="w-4 h-4 text-blue-600 bg-slate-100 border-slate-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-slate-800 focus:ring-2 dark:bg-slate-700 dark:border-slate-600"
/>
)}
</div>
<div className="flex-1">
<label
htmlFor={purpose.id}
className="text-sm font-medium text-slate-800 dark:text-white cursor-pointer"
>
{purpose.name}
{purpose.required && (
<span className="ml-1 text-xs text-green-600 dark:text-green-400">
(Required)
</span>
)}
</label>
<p className="text-xs text-slate-600 dark:text-slate-300 mt-1">
{purpose.description}
</p>
</div>
</div>
))}
</div>
<div className="flex flex-col sm:flex-row gap-3 justify-end">
<button
onClick={handleEssentialOnly}
className="px-4 py-3 text-sm font-medium text-slate-600 hover:text-slate-800 dark:text-slate-300 dark:hover:text-white border border-slate-300 dark:border-slate-600 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
>
Essential Only
</button>
<button
onClick={handleCustomSave}
className="px-4 py-3 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
Save Preferences
</button>
</div>
</div>
)}
</div>
</div>
);
};
export default ConsentBanner;

View File

@@ -0,0 +1,75 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log the error for debugging
this.setState({
error: error,
errorInfo: errorInfo
});
// You can also log the error to an error reporting service here
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
// Fallback UI
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 text-center">
<div className="mb-4">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/20">
<svg className="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
Something went wrong
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
The application encountered an error. This might be due to browser compatibility issues.
</p>
<div className="space-y-3">
<button
onClick={() => window.location.reload()}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
>
Reload Page
</button>
<button
onClick={() => this.setState({ hasError: false, error: null, errorInfo: null })}
className="w-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 font-medium py-2 px-4 rounded-md transition-colors"
>
Try Again
</button>
</div>
<div className="mt-4 text-xs text-gray-500 dark:text-gray-400">
If you're using Telegram's built-in browser, try opening this link in your default browser for better compatibility.
</div>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -1,15 +1,25 @@
import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Home, Hash, FileSpreadsheet, Wand2, GitCompare, Menu, X, LinkIcon, Code2, ChevronDown, Type, Edit3 } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
import { useLocation } from 'react-router-dom';
import ToolSidebar from './ToolSidebar';
import NavigationConfirmModal from './NavigationConfirmModal';
import useNavigationGuard from '../hooks/useNavigationGuard';
import { Menu, X, ChevronDown, Terminal, Sparkles, Home } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
import SEOHead from './SEOHead';
import ConsentBanner from './ConsentBanner';
import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
import { useAnalytics } from '../hooks/useAnalytics';
const Layout = ({ children }) => {
const location = useLocation();
const { showModal, pendingNavigation, handleConfirm, handleCancel, hasUnsavedData, navigateWithGuard } = useNavigationGuard();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const dropdownRef = useRef(null);
// Initialize analytics tracking
useAnalytics();
const isActive = (path) => {
return location.pathname === path;
};
@@ -34,84 +44,114 @@ const Layout = ({ children }) => {
setIsDropdownOpen(false);
}, [location.pathname]);
const tools = [
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
{ path: '/beautifier', name: 'Beautifier Tool', icon: Wand2, description: 'Beautify/minify code' },
{ path: '/diff', name: 'Diff Tool', icon: GitCompare, description: 'Compare text differences' },
{ path: '/text-length', name: 'Text Length Checker', icon: Type, description: 'Analyze text length & stats' },
];
// Check if we're on a tool page (not homepage)
const isToolPage = location.pathname !== '/';
// Check if we're on invoice preview page (no sidebar needed)
const isInvoicePreviewPage = location.pathname === '/invoice-preview';
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col">
<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 flex-col">
{/* SEO Head Management */}
<SEOHead />
{/* Header */}
<header className="sticky top-0 z-50 bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<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={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">
<Link to="/" className="flex items-center space-x-2">
<Code2 className="h-8 w-8 text-primary-600" />
<span className="text-xl font-bold text-gray-900 dark:text-white">
DevTools
</span>
</Link>
<button onClick={() => navigateWithGuard('/')} className="flex items-center group">
<div className="relative">
<div className="absolute inset-0 rounded-lg blur opacity-20 group-hover:opacity-40 transition-opacity"></div>
<div className="relative p-2">
<img
src="/logo.svg"
alt={SITE_CONFIG.title}
className="h-8 w-auto"
style={{ maxWidth: '150px' }}
onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
}}
/>
<div className="hidden items-center space-x-3">
<Terminal className="h-6 w-6 text-blue-500" />
<span className="text-xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
{SITE_CONFIG.title}
</span>
</div>
</div>
</div>
</button>
<div className="flex items-center space-x-4">
{/* Desktop Navigation - only show on homepage */}
{!isToolPage && (
<nav className="hidden md:flex items-center space-x-6">
<Link
to="/"
className={`flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
<button
onClick={() => {
setIsDropdownOpen(false);
navigateWithGuard('/');
}}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 ${
isActive('/')
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<Home className="h-4 w-4" />
<span>Home</span>
</Link>
</button>
{/* Tools Dropdown */}
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors"
className="flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300"
>
<Sparkles className="h-4 w-4" />
<span>Tools</span>
<ChevronDown className={`h-4 w-4 transition-transform ${
<ChevronDown className={`h-4 w-4 transition-transform duration-300 ${
isDropdownOpen ? 'rotate-180' : ''
}`} />
</button>
{/* Dropdown Menu */}
{isDropdownOpen && (
<div className="absolute top-full left-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-2 z-50">
{tools.map((tool) => {
const IconComponent = tool.icon;
return (
<Link
key={tool.path}
to={tool.path}
onClick={() => setIsDropdownOpen(false)}
className={`flex items-center space-x-3 px-4 py-3 text-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${
isActive(tool.path)
? 'bg-primary-50 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'text-gray-700 dark:text-gray-300'
}`}
>
<IconComponent className="h-4 w-4" />
<div>
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{tool.description}</div>
</div>
</Link>
);
})}
<div className="absolute top-full left-0 mt-3 w-80 bg-white/90 dark:bg-slate-800/90 backdrop-blur-md rounded-2xl shadow-2xl border border-slate-200/50 dark:border-slate-700/50 py-3 z-50 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-purple-50/50 dark:from-slate-800/50 dark:to-slate-700/50"></div>
<div className="relative">
{TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return (
<button
key={tool.path}
onClick={() => {
setIsDropdownOpen(false);
navigateWithGuard(tool.path);
}}
className={`group flex items-center space-x-4 px-4 py-3 text-sm hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
: 'text-slate-700 dark:text-slate-300'
}`}
>
<div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm group-hover:scale-110 transition-transform duration-300`}>
<IconComponent className="h-4 w-4 text-white" />
</div>
<div className="flex-1">
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-500 dark:text-slate-400">{tool.description}</div>
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ChevronDown className="h-4 w-4 -rotate-90 text-slate-400" />
</div>
</button>
);
})}
</div>
</div>
)}
</div>
@@ -123,7 +163,7 @@ const Layout = ({ children }) => {
{/* Mobile Menu Button */}
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="md:hidden p-2 rounded-md text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white transition-colors"
className="md:hidden p-2 rounded-xl text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300"
>
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</button>
@@ -134,88 +174,183 @@ const Layout = ({ children }) => {
{/* Mobile Navigation Menu */}
{isMobileMenuOpen && (
<div className="md:hidden bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<>
{/* Overlay */}
<div
className="md:hidden fixed inset-0 bg-black/20 z-30"
onClick={() => setIsMobileMenuOpen(false)}
/>
{/* Menu */}
<div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="space-y-2">
<Link
to="/"
onClick={() => setIsMobileMenuOpen(false)}
className={`flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
isActive('/')
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
}`}
>
<Home className="h-5 w-5" />
<span>Home</span>
</Link>
{/* Non-Tools Section */}
{NON_TOOLS.map((tool) => {
const IconComponent = tool.icon;
return (
<button
key={tool.path}
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<div className={`p-2 rounded-lg ${isActive(tool.path) ? 'bg-white/20' : 'bg-gradient-to-br from-indigo-500 to-purple-500'} shadow-sm`}>
<IconComponent className={`h-4 w-4 ${isActive(tool.path) ? 'text-white' : 'text-white'}`} />
</div>
<span>{tool.name}</span>
</button>
);
})}
<div className="border-t border-gray-200 dark:border-gray-700 pt-2 mt-2">
<div className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider px-3 py-1">
<div className="border-t border-slate-200/50 dark:border-slate-700/50 pt-4 mt-4">
<div className="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider px-4 py-2 flex items-center gap-2">
<Sparkles className="h-3 w-3" />
{isToolPage ? 'Switch Tools' : 'Tools'}
</div>
{tools.map((tool) => {
{TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return (
<Link
<button
key={tool.path}
to={tool.path}
onClick={() => setIsMobileMenuOpen(false)}
className={`flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white'
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<IconComponent className="h-5 w-5" />
<div>
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">{tool.description}</div>
<div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm`}>
<IconComponent className="h-4 w-4 text-white" />
</div>
</Link>
<div className="flex-1">
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-500 dark:text-slate-400">{tool.description}</div>
</div>
</button>
);
})}
</div>
</div>
</div>
</div>
</>
)}
{/* Main Content */}
<div className="flex flex-1">
{/* Tool Sidebar - only show on tool pages */}
{isToolPage && (
<div className="hidden lg:block flex-shrink-0">
<ToolSidebar />
</div>
)}
<div className="flex flex-1 pt-16 min-w-0 w-full max-w-full overflow-x-hidden">
{/* Main Content Area */}
<main className={`flex-1 flex flex-col ${isToolPage ? 'overflow-hidden' : ''}`}>
{isToolPage ? (
<div className="flex-1 overflow-auto">
<div className="px-4 sm:px-6 lg:px-8 py-8">
<main className="flex-1 flex flex-col min-w-0 w-full max-w-full">
{isToolPage && !isInvoicePreviewPage ? (
<div className="block">
<div className="hidden lg:block fixed top-16 left-0 z-[9999]">
<ToolSidebar navigateWithGuard={navigateWithGuard} />
</div>
<div className="flex-1 flex flex-col pl-0 lg:pl-16 min-w-0">
<div className="p-4 sm:p-6 w-full min-w-0 max-w-full overflow-x-hidden">
{children}
</div>
</div>
</div>
) : isInvoicePreviewPage ? (
<div className="flex-1 flex flex-col">
<div className="flex-1">
{children}
</div>
{/* Footer for tool pages - inside scrollable content */}
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center text-gray-600 dark:text-gray-400">
<p>© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.</p>
</div>
</div>
</footer>
</div>
) : (
<div className="flex-1">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex-1 flex flex-col">
<div className="flex-1">
{children}
</div>
{/* Footer for homepage */}
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center text-gray-600 dark:text-gray-400">
<p>© {new Date().getFullYear()} Dewe Toolsites - Developer Tools.</p>
{/* Global Footer for Homepage */}
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center">
<div className="flex items-center justify-center mb-4">
<div className="relative">
<div className="absolute inset-0 rounded-lg blur opacity-20"></div>
<div className="relative p-2">
<img
src="/icon-192x192.png"
alt={SITE_CONFIG.title}
className="h-16 w-auto"
style={{ maxWidth: '100px' }}
onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
}}
/>
<div className="hidden items-center gap-3">
<Terminal className="h-5 w-5 text-blue-500" />
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
{SITE_CONFIG.title}
</span>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-center gap-2 mb-3">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-sm text-slate-500 dark:text-slate-500 mb-4">
Built with for developers worldwide
</p>
<div className="flex flex-col items-center gap-4">
<div className="flex justify-center items-center gap-6 text-xs text-slate-400 dark:text-slate-500">
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
<span>100% Client-Side</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
<span>Privacy First</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
<span>Open Source</span>
</div>
</div>
<div className="flex items-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Release Notes
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/privacy')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/terms')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</button>
</div>
</div>
</div>
</div>
</footer>
@@ -223,6 +358,70 @@ const Layout = ({ children }) => {
)}
</main>
</div>
{/* Footer for Tool Pages */}
{isToolPage && (
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<img
src="/icon-192x192.png"
alt={SITE_CONFIG.title}
className="h-16 w-auto"
style={{ maxWidth: '100px' }}
onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
}}
/>
</div>
<div className="flex items-center justify-center gap-2 mb-2">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-xs font-medium text-slate-600 dark:text-slate-400">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<div className="flex items-center justify-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Release Notes
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/privacy')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/terms')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</button>
</div>
</div>
</div>
</footer>
)}
{/* GDPR Consent Banner */}
<ConsentBanner />
{/* Navigation Confirmation Modal */}
<NavigationConfirmModal
isOpen={showModal}
onConfirm={handleConfirm}
onCancel={handleCancel}
targetPath={pendingNavigation?.to}
hasData={hasUnsavedData}
/>
</div>
);
};

View File

@@ -20,7 +20,6 @@ import {
ToggleLeft,
FileText,
Zap,
Copy,
Eye,
Code,
Maximize,
@@ -38,17 +37,6 @@ const CustomNode = ({ data, selected }) => {
// Check if value contains HTML
const isHtmlContent = data.value && typeof data.value === 'string' &&
(data.value.includes('<') && data.value.includes('>'));
// Copy value to clipboard
const copyValue = async () => {
if (data.value) {
try {
await navigator.clipboard.writeText(String(data.value));
} catch (err) {
console.error('Failed to copy:', err);
}
}
};
const getIcon = () => {
switch (data.type) {
case 'object':
@@ -131,21 +119,12 @@ const CustomNode = ({ data, selected }) => {
)}
<div className="flex items-start space-x-2 group">
<div className="flex-shrink-0 flex flex-col items-center space-y-1">
<div className="mt-0.5">
{getIcon()}
</div>
{/* Copy button positioned below icon */}
{data.value && (
<button
onClick={copyValue}
className="bg-blue-500 hover:bg-blue-600 text-white rounded-full p-1 opacity-80 hover:opacity-100 transition-all shadow-md"
title="Copy value to clipboard"
>
<Copy className="h-2.5 w-2.5" />
</button>
)}
{/* Icon - First flex item */}
<div className="flex-shrink-0 mt-0.5">
{getIcon()}
</div>
{/* Content - Second flex item */}
<div className="flex-1 min-w-0">
<div className="font-medium text-xs break-words">
{data.label}
@@ -170,6 +149,7 @@ const CustomNode = ({ data, selected }) => {
</div>
)}
</div>
</div>
{/* Output handle (right side) */}
@@ -474,7 +454,7 @@ const MindmapView = React.memo(({ data }) => {
return (
<div className={`w-full border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 ${
isFullscreen
? 'fixed inset-0 z-50 rounded-none'
? 'fixed inset-0 z-[99999] rounded-none'
: 'h-[600px]'
}`}>
<ReactFlow

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;

View File

@@ -0,0 +1,115 @@
import React from 'react';
import { AlertTriangle } from 'lucide-react';
const NavigationConfirmModal = ({ isOpen, onConfirm, onCancel, targetPath, hasData }) => {
if (!isOpen) return null;
const getDataSummary = () => {
try {
const invoiceData = localStorage.getItem('currentInvoice');
const objectData = localStorage.getItem('objectEditorData');
const tableData = localStorage.getItem('tableEditorData');
const summary = [];
if (invoiceData) {
const parsed = JSON.parse(invoiceData);
if (parsed.invoiceNumber) summary.push(`Invoice #${parsed.invoiceNumber}`);
if (parsed.company?.name) summary.push(`Company information (${parsed.company.name})`);
if (parsed.client?.name) summary.push(`Client information (${parsed.client.name})`);
if (parsed.items?.length > 0) summary.push(`${parsed.items.length} line items`);
}
if (objectData) {
const parsed = JSON.parse(objectData);
if (parsed && Object.keys(parsed).length > 0) {
summary.push(`Object data with ${Object.keys(parsed).length} properties`);
}
}
if (tableData) {
const parsed = JSON.parse(tableData);
if (parsed && parsed.length > 0) {
summary.push(`Table data with ${parsed.length} rows`);
}
}
return summary;
} catch (error) {
return ['Unsaved data'];
}
};
const dataSummary = getDataSummary();
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-amber-50 dark:bg-amber-900/20">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<AlertTriangle className="h-6 w-6 text-amber-600 dark:text-amber-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-amber-900 dark:text-amber-100">
Confirm Navigation
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300">
You have unsaved data that will be lost
</p>
</div>
</div>
</div>
{/* Content */}
<div className="px-6 py-4">
<p className="text-gray-700 dark:text-gray-300 mb-4">
You currently have unsaved data that will be lost if you leave this page. Are you sure you want to continue?
</p>
{dataSummary.length > 0 && (
<div className="bg-gray-50 dark:bg-gray-700 rounded-md p-3 mb-4">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
You currently have:
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
{dataSummary.map((item, index) => (
<li key={index} className="flex items-center">
<span className="w-1.5 h-1.5 bg-amber-500 rounded-full mr-2 flex-shrink-0"></span>
{item}
</li>
))}
</ul>
</div>
)}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-3">
<p className="text-blue-800 dark:text-blue-200 text-sm">
<strong>Tip:</strong> Consider saving or exporting your current work before proceeding.
</p>
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600 flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-500 transition-colors"
>
Cancel
</button>
<button
onClick={onConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 rounded-md transition-colors flex items-center gap-2"
>
<AlertTriangle className="h-4 w-4" />
Continue & Lose Data
</button>
</div>
</div>
</div>
);
};
export default NavigationConfirmModal;

View File

@@ -1,9 +1,10 @@
import React, { useState, useMemo } from 'react';
import { ChevronLeft, Braces, List, Type, Hash, ToggleLeft, Minus } from 'lucide-react';
import React, { useState } from 'react';
import { ChevronLeft, Braces, List, Type, Hash, ToggleLeft, Minus, Eye, Code, Copy, Check } from 'lucide-react';
const PostmanTable = ({ data, title = "JSON Data" }) => {
const [currentPath, setCurrentPath] = useState([]);
const [selectedRowIndex, setSelectedRowIndex] = useState(null);
const [renderHtml, setRenderHtml] = useState(true);
const [copiedItems, setCopiedItems] = useState(new Set());
// Get current data based on path
const getCurrentData = () => {
@@ -45,10 +46,52 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
const headers = getArrayHeaders();
// Check if value contains HTML
const isHtmlContent = (value) => {
return value && typeof value === 'string' &&
(value.includes('<') && value.includes('>')) &&
/<[^>]+>/.test(value);
};
// Copy value to clipboard
const copyToClipboard = async (value, itemId) => {
try {
const textValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
await navigator.clipboard.writeText(textValue);
// Show feedback
setCopiedItems(prev => new Set([...prev, itemId]));
setTimeout(() => {
setCopiedItems(prev => {
const newSet = new Set(prev);
newSet.delete(itemId);
return newSet;
});
}, 2500);
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
// Show feedback for fallback too
setCopiedItems(prev => new Set([...prev, itemId]));
setTimeout(() => {
setCopiedItems(prev => {
const newSet = new Set(prev);
newSet.delete(itemId);
return newSet;
});
}, 2500);
}
};
// Handle row click - navigate to item details
const handleRowClick = (index, key = null) => {
if (isArrayView) {
setSelectedRowIndex(index);
setCurrentPath([...currentPath, index]);
} else if (isObjectView && key) {
// Navigate into object property
@@ -61,7 +104,6 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
if (currentPath.length > 0) {
const newPath = currentPath.slice(0, -1);
setCurrentPath(newPath);
setSelectedRowIndex(null);
}
};
@@ -72,11 +114,9 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
setCurrentPath([]);
} else {
// Click on specific breadcrumb - navigate to that level
// Adjust for the "Root" prefix in breadcrumb
const newPath = currentPath.slice(0, index);
setCurrentPath(newPath);
}
setSelectedRowIndex(null);
};
// Generate breadcrumb
@@ -92,11 +132,32 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
return parts;
};
// Format value for display
// Format value for display in table (truncated)
const formatValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'string') return value;
if (typeof value === 'string') {
// Truncate long strings for table display
if (value.length > 100) {
return value.substring(0, 100) + '...';
}
// Replace newlines with spaces for single-line display
return value.replace(/\n/g, ' ').replace(/\s+/g, ' ');
}
if (typeof value === 'boolean') return value.toString();
if (typeof value === 'number') return value.toString();
if (typeof value === 'object') {
if (Array.isArray(value)) return `Array(${value.length})`;
return `Object(${Object.keys(value).length})`;
}
return String(value);
};
// Format value for display in details view (full text)
const formatFullValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'string') return value; // Show full string without truncation
if (typeof value === 'boolean') return value.toString();
if (typeof value === 'number') return value.toString();
if (typeof value === 'object') {
@@ -158,24 +219,54 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
}
};
// Check if value is a complex object (not a primitive)
const isComplexValue = (value) => {
return value !== null && typeof value === 'object';
};
// Render value with appropriate styling
// Render value with appropriate styling (for table view)
const renderValue = (value) => {
const typeStyle = getTypeStyle(value);
const formattedValue = formatValue(value);
const hasHtml = isHtmlContent(value);
return (
<span className={`inline-flex items-center space-x-1 px-2 py-1 rounded-full text-xs font-medium ${typeStyle.color}`}>
{typeStyle.icon}
<span>{formattedValue}</span>
<span className={`inline-flex items-center space-x-1 px-2 py-1 rounded text-xs font-medium ${typeStyle.color}`}>
<span className="flex-shrink-0 w-3 h-3 flex items-center justify-center">
{typeStyle.icon}
</span>
<span>
{hasHtml && renderHtml ? (
<div dangerouslySetInnerHTML={{ __html: String(value) }} />
) : (
formattedValue
)}
</span>
</span>
);
};
// Render value with full text (for detail view)
const renderFullValue = (value) => {
const typeStyle = getTypeStyle(value);
const formattedValue = formatFullValue(value);
const hasHtml = isHtmlContent(value);
return (
<div className="relative">
<span className={`inline-flex items-center space-x-2 px-2 py-1 rounded text-xs font-medium ${typeStyle.color}`}>
<span className="flex-shrink-0 w-3 h-3 flex items-center justify-center">
{typeStyle.icon}
</span>
<span className="whitespace-pre-wrap break-words flex-1">
{hasHtml && renderHtml ? (
<div dangerouslySetInnerHTML={{ __html: String(value) }} />
) : (
formattedValue
)}
</span>
</span>
</div>
);
};
// Get value type
const getValueType = (value) => {
if (value === null) return 'null';
@@ -184,7 +275,7 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
};
return (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Header with Breadcrumb */}
<div className="bg-gray-50 dark:bg-gray-700 px-4 py-3 border-b border-gray-200 dark:border-gray-600">
<div className="flex items-center justify-between">
@@ -217,19 +308,50 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
))}
</div>
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{isArrayView && `${currentData.length} items`}
{isObjectView && `${Object.keys(currentData).length} properties`}
<div className="flex items-center justify-between">
<div className="text-sm text-gray-500 dark:text-gray-400">
{isArrayView && `${currentData.length} items`}
{isObjectView && `${Object.keys(currentData).length} properties`}
</div>
{/* Global HTML/Raw Toggle */}
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-500 dark:text-gray-400">Text Display:</span>
<div className="flex rounded-md overflow-hidden border border-gray-300 dark:border-gray-600">
<button
onClick={() => setRenderHtml(true)}
className={`px-2 py-1 text-xs transition-colors ${
renderHtml
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
}`}
title="Render HTML"
>
<Eye className="h-3 w-3" />
</button>
<button
onClick={() => setRenderHtml(false)}
className={`px-2 py-1 text-xs transition-colors ${
!renderHtml
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-500'
}`}
title="Show Raw Text"
>
<Code className="h-3 w-3" />
</button>
</div>
</div>
</div>
</div>
</div>
{/* Content */}
<div className="overflow-auto max-h-96">
<div className="overflow-auto">
{isArrayView ? (
// Horizontal table for arrays
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-12">
#
@@ -272,7 +394,7 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
) : isObjectView ? (
// Vertical key-value table for objects
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-1/3">
Key
@@ -280,6 +402,9 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Value
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-16">
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@@ -299,7 +424,23 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
{key}
</td>
<td className="px-4 py-3 text-sm">
{renderValue(value)}
{renderFullValue(value)}
</td>
<td className="px-4 py-3 text-sm">
<button
onClick={(e) => {
e.stopPropagation(); // Prevent row click
copyToClipboard(value, `${currentPath.join('.')}.${key}`);
}}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors"
title={copiedItems.has(`${currentPath.join('.')}.${key}`) ? "Copied!" : "Copy value"}
>
{copiedItems.has(`${currentPath.join('.')}.${key}`) ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3 text-gray-500 dark:text-gray-400" />
)}
</button>
</td>
</tr>
);
@@ -310,11 +451,8 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
// Fallback for primitive values
<div className="p-4">
<div className="text-center text-gray-500 dark:text-gray-400">
<div className="text-lg font-mono text-gray-900 dark:text-gray-100">
{formatValue(currentData)}
</div>
<div className="text-sm mt-2">
Type: {getValueType(currentData)}
<div className="text-lg font-mono text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words">
{formatFullValue(currentData)}
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import { ChevronRight, ChevronDown, Copy, Search, Filter } from 'lucide-react';
import CopyButton from './CopyButton';
@@ -8,7 +8,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
const [filterType, setFilterType] = useState('all');
// Flatten the data structure for table display
const flattenData = (obj, path = '', level = 0) => {
const flattenData = useCallback((obj, path = '', level = 0) => {
const result = [];
if (obj === null || obj === undefined) {
@@ -74,7 +74,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
}
return result;
};
}, [expandedPaths]);
const toggleExpanded = (path) => {
const newExpanded = new Set(expandedPaths);
@@ -86,7 +86,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
setExpandedPaths(newExpanded);
};
const flatData = useMemo(() => flattenData(data), [data, expandedPaths]);
const flatData = useMemo(() => flattenData(data), [data, flattenData]);
const filteredData = useMemo(() => {
return flatData.filter(item => {
@@ -129,7 +129,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
};
const getKeyDisplay = (key, level) => {
const parts = key.split(/[.\[\]]+/).filter(Boolean);
const parts = key.split(/[.[\]]+/).filter(Boolean);
return parts[parts.length - 1] || key;
};

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;

130
src/components/SEO.js Normal file
View File

@@ -0,0 +1,130 @@
import React from 'react';
import { Helmet } from 'react-helmet-async';
import TOOL_FAQS from '../data/faqs';
const SEO = ({
title,
description,
keywords,
path = '/',
type = 'website',
image = 'https://dewe.dev/og-image.png',
toolId = null
}) => {
const siteUrl = 'https://dewe.dev';
const fullUrl = `${siteUrl}${path}`;
const fullTitle = title ? `${title} | Developer Tools` : 'Developer Tools - Essential Web Developer Utilities';
const defaultDescription = 'Free online developer tools for JSON, CSV, Base64, URL encoding, code beautification, and more. Privacy-first, no data storage, all processing in your browser.';
const metaDescription = description || defaultDescription;
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;
// 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 (
<Helmet>
{/* Basic Meta Tags */}
<title>{fullTitle}</title>
<meta name="description" content={metaDescription} />
<meta name="keywords" content={metaKeywords} />
<link rel="canonical" href={fullUrl} />
{/* Open Graph / Facebook */}
<meta property="og:type" content={type} />
<meta property="og:url" content={fullUrl} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={metaDescription} />
<meta property="og:image" content={image} />
<meta property="og:site_name" content="Developer Tools" />
{/* Twitter Card */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content={fullUrl} />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={metaDescription} />
<meta name="twitter:image" content={image} />
{/* Additional SEO Tags */}
<meta name="robots" content="index, follow" />
<meta name="googlebot" content="index, follow" />
<meta name="author" content="Developer Tools" />
<meta name="language" content="English" />
{/* JSON-LD Structured Data - WebApplication */}
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "WebApplication",
"name": fullTitle,
"description": metaDescription,
"url": fullUrl,
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Any",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"provider": {
"@type": "Organization",
"name": "Developer Tools",
"url": siteUrl
}
})}
</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>
);
};
export default SEO;

75
src/components/SEOHead.js Normal file
View File

@@ -0,0 +1,75 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { generateMetaTags } from '../utils/seo';
// SEO Head component that manually updates document head
// This works without additional dependencies until we add react-helmet-async
const SEOHead = () => {
const location = useLocation();
useEffect(() => {
const { title, meta, link, structuredData } = generateMetaTags(location.pathname);
// Update document title
document.title = title;
// Remove existing meta tags that we manage
const existingMeta = document.querySelectorAll('meta[data-seo="true"]');
existingMeta.forEach(tag => tag.remove());
const existingLinks = document.querySelectorAll('link[data-seo="true"]');
existingLinks.forEach(tag => tag.remove());
const existingStructuredData = document.querySelectorAll('script[data-seo="structured-data"]');
existingStructuredData.forEach(tag => tag.remove());
// Add new meta tags
meta.forEach(({ name, property, content }) => {
const metaTag = document.createElement('meta');
if (name) metaTag.setAttribute('name', name);
if (property) metaTag.setAttribute('property', property);
metaTag.setAttribute('content', content);
metaTag.setAttribute('data-seo', 'true');
document.head.appendChild(metaTag);
});
// Add canonical link
link.forEach(({ rel, href }) => {
const linkTag = document.createElement('link');
linkTag.setAttribute('rel', rel);
linkTag.setAttribute('href', href);
linkTag.setAttribute('data-seo', 'true');
document.head.appendChild(linkTag);
});
// Add structured data
if (structuredData) {
const script = document.createElement('script');
script.type = 'application/ld+json';
script.setAttribute('data-seo', 'structured-data');
script.textContent = JSON.stringify(structuredData);
document.head.appendChild(script);
}
// Add preconnect for performance
const preconnectLinks = [
'https://www.googletagmanager.com',
'https://www.google-analytics.com'
];
preconnectLinks.forEach(href => {
if (!document.querySelector(`link[rel="preconnect"][href="${href}"]`)) {
const preconnect = document.createElement('link');
preconnect.rel = 'preconnect';
preconnect.href = href;
preconnect.setAttribute('data-seo', 'true');
document.head.appendChild(preconnect);
}
});
}, [location.pathname]);
return null; // This component doesn't render anything
};
export default SEOHead;

View File

@@ -1,17 +1,27 @@
import React, { useState, useEffect } from 'react';
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces } from 'lucide-react';
import React, { useState, useEffect, useRef } from 'react';
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces, Edit3, X, Eye, Pencil } from 'lucide-react';
const StructuredEditor = ({ onDataChange, initialData = {} }) => {
const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyProp = false }) => {
const [data, setData] = useState(initialData);
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
const [fieldTypes, setFieldTypes] = useState({}); // Track intended types for fields
const isInternalUpdate = useRef(false);
const [nestedEditModal, setNestedEditModal] = useState(null); // { path, value, type: 'json' | 'serialized' }
const [nestedData, setNestedData] = useState(null);
// 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
const readOnly = readOnlyProp !== false ? readOnlyProp : !editMode;
// Update internal data when initialData prop changes
// Update internal data when initialData prop changes (but not from internal updates)
useEffect(() => {
console.log('📥 INITIAL DATA CHANGED:', {
keys: Object.keys(initialData),
hasData: Object.keys(initialData).length > 0,
data: initialData
});
// Skip update if this change came from internal editor actions
if (isInternalUpdate.current) {
isInternalUpdate.current = false;
return;
}
setData(initialData);
// Expand root node if there's data
if (Object.keys(initialData).length > 0) {
@@ -20,11 +30,178 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
}, [initialData]);
const updateData = (newData) => {
console.log('📊 DATA UPDATE:', { keys: Object.keys(newData), totalProps: JSON.stringify(newData).length });
isInternalUpdate.current = true; // Mark as internal update
setData(newData);
onDataChange(newData);
};
// PHP serialize/unserialize functions
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') {
const escapedData = data.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
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;';
};
const phpUnserialize = (str) => {
let index = 0;
const parseValue = () => {
if (index >= str.length) throw new Error('Unexpected end of string');
const type = str[index];
if (type === 'N') {
index += 2;
return null;
}
if (str[index + 1] !== ':') throw new Error(`Expected ':' after type '${type}'`);
index += 2;
switch (type) {
case 'b':
const boolVal = str[index] === '1';
index += 2;
return boolVal;
case 'i':
let intStr = '';
while (index < str.length && str[index] !== ';') intStr += str[index++];
index++;
return parseInt(intStr);
case 'd':
let floatStr = '';
while (index < str.length && str[index] !== ';') floatStr += str[index++];
index++;
return parseFloat(floatStr);
case 's':
let lenStr = '';
while (index < str.length && str[index] !== ':') lenStr += str[index++];
index++;
if (str[index] !== '"') throw new Error('Expected opening quote');
index++;
const byteLength = parseInt(lenStr);
if (byteLength === 0) {
index += 2;
return '';
}
let endQuotePos = -1;
for (let i = index; i < str.length - 1; i++) {
if (str[i] === '"' && str[i + 1] === ';') {
endQuotePos = i;
break;
}
}
if (endQuotePos === -1) throw new Error('Could not find closing quote');
const strValue = str.substring(index, endQuotePos);
index = endQuotePos + 2;
return strValue.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
case 'a':
let countStr = '';
while (index < str.length && str[index] !== ':') countStr += str[index++];
const count = parseInt(countStr);
index += 2;
const result = {};
let isArray = true;
for (let i = 0; i < count; i++) {
const key = parseValue();
const value = parseValue();
result[key] = value;
if (key !== i) isArray = false;
}
index++;
return isArray ? Object.values(result) : result;
default:
throw new Error(`Unknown type: ${type}`);
}
};
return parseValue();
};
// Detect if a string contains JSON or serialized data
const detectNestedData = (value) => {
if (typeof value !== 'string' || value.length < 5) return null;
// Try JSON first
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null) {
return { type: 'json', data: parsed };
}
} catch (e) {
// Not JSON, continue
}
// Try PHP serialized
try {
// Check if it looks like PHP serialized format
if (/^[abidsNO]:[^;]*;/.test(value) || /^a:\d+:\{/.test(value)) {
const parsed = phpUnserialize(value);
if (typeof parsed === 'object' && parsed !== null) {
return { type: 'serialized', data: parsed };
}
}
} catch (e) {
// Not serialized
}
return null;
};
// Open nested editor modal
const openNestedEditor = (value, path) => {
const detected = detectNestedData(value);
if (detected) {
setNestedEditModal({ path, value, type: detected.type });
setNestedData(detected.data);
}
};
// Save nested editor changes
const saveNestedEdit = () => {
if (!nestedEditModal || !nestedData) return;
// Convert back to string based on type
let stringValue;
if (nestedEditModal.type === 'json') {
stringValue = JSON.stringify(nestedData);
} else if (nestedEditModal.type === 'serialized') {
stringValue = phpSerialize(nestedData);
}
// Update the value in the main data
updateValue(stringValue, nestedEditModal.path);
// Close modal
setNestedEditModal(null);
setNestedData(null);
};
// Close nested editor modal
const closeNestedEditor = () => {
setNestedEditModal(null);
setNestedData(null);
};
const toggleNode = (path) => {
const newExpanded = new Set(expandedNodes);
if (newExpanded.has(path)) {
@@ -36,8 +213,6 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
};
const addProperty = (obj, path) => {
console.log('🔧 ADD PROPERTY - Before:', { path, dataKeys: Object.keys(data), objKeys: Object.keys(obj) });
const pathParts = path.split('.');
const newData = { ...data };
let current = newData;
@@ -52,8 +227,6 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
const newKey = `property${keys.length + 1}`;
current[newKey] = '';
console.log('🔧 ADD PROPERTY - After:', { path, newKey, dataKeys: Object.keys(newData), targetKeys: Object.keys(current) });
updateData(newData);
setExpandedNodes(new Set([...expandedNodes, path]));
};
@@ -82,24 +255,36 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
const newData = { ...data };
let current = newData;
// Navigate to the parent object/array
for (let i = 1; i < pathParts.length; i++) {
if (i === pathParts.length - 1) {
delete current[key];
} else {
current = current[pathParts[i]];
}
current = current[pathParts[i]];
}
if (pathParts.length === 1) {
delete newData[key];
// Remove field type tracking for the removed property
const removedPath = parentPath === 'root' ? `root.${key}` : `${parentPath}.${key}`;
const newFieldTypes = { ...fieldTypes };
delete newFieldTypes[removedPath];
setFieldTypes(newFieldTypes);
// Delete the property/item from the parent
if (Array.isArray(current)) {
// For arrays, remove by index and reindex
current.splice(parseInt(key), 1);
} else {
// For objects, delete the property
delete current[key];
}
// Check if we're removing from root level and it's the last property
if (parentPath === 'root' && Object.keys(newData).length === 0) {
// Add an empty property to maintain initial state, like TableEditor maintains at least one row
newData[''] = '';
}
updateData(newData);
};
const updateValue = (value, path) => {
console.log('✏️ UPDATE VALUE:', { path, value, currentType: typeof getValue(path) });
const pathParts = path.split('.');
const newData = { ...data };
let current = newData;
@@ -121,29 +306,36 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
} else if (currentValue === null) {
current[key] = value === 'null' ? null : value;
} else {
// For strings and initial empty values, use auto-detection
// For strings and initial empty values, use smart detection
if (currentValue === '' || currentValue === undefined) {
if (value === 'true' || value === 'false') {
current[key] = value === 'true';
} else if (value === 'null') {
current[key] = null;
} else if (!isNaN(value) && value !== '' && value.trim() !== '') {
current[key] = Number(value);
} else {
// Check if this is a newly added property (starts with "property" + number)
const isNewProperty = typeof key === 'string' && key.match(/^property\d+$/);
if (isNewProperty) {
// New properties added by user are always strings (no auto-detection)
current[key] = value;
} else {
// Existing properties from loaded data - use auto-detection
if (value === 'true' || value === 'false') {
current[key] = value === 'true';
} else if (value === 'null') {
current[key] = null;
} else if (!isNaN(value) && value !== '' && value.trim() !== '') {
current[key] = Number(value);
} else {
current[key] = value;
}
}
} else {
// Existing non-empty values - preserve as string unless user explicitly changes type
current[key] = value;
}
}
console.log('✏️ UPDATE VALUE - Result:', { path, newValue: current[key], newType: typeof current[key] });
updateData(newData);
};
const changeType = (newType, path) => {
console.log('🔄 CHANGE TYPE:', { path, newType, currentValue: getValue(path) });
const pathParts = path.split('.');
const newData = { ...data };
let current = newData;
@@ -155,6 +347,11 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
const key = pathParts[pathParts.length - 1];
const currentValue = current[key];
// Track the intended type for this field
const newFieldTypes = { ...fieldTypes };
newFieldTypes[path] = newType;
setFieldTypes(newFieldTypes);
// Try to preserve value when changing types if possible
switch (newType) {
case 'string':
@@ -192,18 +389,28 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
current[key] = '';
}
console.log('🔄 CHANGE TYPE - Result:', { path, newValue: current[key], actualType: typeof current[key] });
updateData(newData);
setExpandedNodes(new Set([...expandedNodes, path]));
};
const getValue = (path) => {
const pathParts = path.split('.');
let current = data;
for (let i = 1; i < pathParts.length; i++) {
current = current[pathParts[i]];
// Helper function to display string values with proper unescaping
const getDisplayValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return '';
const stringValue = value.toString();
// If it's a string, unescape common JSON escape sequences for display
if (typeof value === 'string') {
return stringValue
.replace(/\\"/g, '"') // Unescape quotes
.replace(/\\'/g, "'") // Unescape single quotes
.replace(/\\\//g, '/') // Unescape forward slashes
.replace(/\\\\/g, '\\'); // Unescape backslashes (do this last)
}
return current;
return stringValue;
};
const renameKey = (oldKey, newKey, path) => {
@@ -223,6 +430,16 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
return; // Don't rename if key already exists
}
// Update field type tracking for renamed key
const oldPath = path;
const newPath = path.replace(new RegExp(`\\.${oldKey}$`), `.${newKey}`);
const newFieldTypes = { ...fieldTypes };
if (newFieldTypes[oldPath]) {
newFieldTypes[newPath] = newFieldTypes[oldPath];
delete newFieldTypes[oldPath];
setFieldTypes(newFieldTypes);
}
// Rename the key
const value = current[oldKey];
delete current[oldKey];
@@ -300,13 +517,19 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
const canExpand = typeof value === 'object' && value !== null;
// Check if parent is an array by looking at the parent path
const isArrayItem = parentPath !== 'root' && (() => {
const parentPathParts = parentPath.split('.');
let current = data;
for (let i = 1; i < parentPathParts.length; i++) {
current = current[parentPathParts[i]];
const isArrayItem = (() => {
if (parentPath === 'root') {
// If parent is root, check if root data is an array
return Array.isArray(data);
} else {
// Navigate to parent and check if it's an array
const parentPathParts = parentPath.split('.');
let current = data;
for (let i = 1; i < parentPathParts.length; i++) {
current = current[parentPathParts[i]];
}
return Array.isArray(current);
}
return Array.isArray(current);
})();
return (
@@ -315,7 +538,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
{canExpand && (
<button
onClick={() => toggleNode(path)}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-gray-700 dark:text-gray-300"
>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
@@ -340,73 +563,108 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
// Object properties: icon + editable key + colon (compact)
<>
{getTypeIcon(value)}
<input
type="text"
defaultValue={key}
onBlur={(e) => {
const newKey = e.target.value.trim();
if (newKey && newKey !== key) {
renameKey(key, newKey, path);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.target.blur(); // Trigger blur to save changes
}
}}
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-0"
placeholder="Property name"
style={{width: '120px'}} // Fixed width for consistency
/>
<span className="text-gray-500 hidden sm:inline">:</span>
{readOnly ? (
<span className="px-2 py-1 text-sm text-gray-900 dark:text-gray-100 font-mono">
{key}
</span>
) : (
<input
type="text"
defaultValue={key}
onBlur={(e) => {
const newKey = e.target.value.trim();
if (newKey && newKey !== key) {
renameKey(key, newKey, path);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.target.blur(); // Trigger blur to save changes
}
}}
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-0"
placeholder="Property name"
style={{width: '120px'}} // Fixed width for consistency
/>
)}
<span className="text-gray-500 inline">:</span>
</>
)}
{!canExpand ? (
typeof value === 'boolean' ? (
<div className="flex-1 flex items-center space-x-2">
<button
onClick={() => updateValue((!value).toString(), path)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
value ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
value ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
<span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
{value.toString()}
</span>
{readOnly ? (
<span className="text-sm text-gray-900 dark:text-gray-100 font-mono">
{value.toString()}
</span>
) : (
<>
<button
onClick={() => updateValue((!value).toString(), path)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
value ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-600'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
value ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
<span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
{value.toString()}
</span>
</>
)}
</div>
) : (
typeof value === 'string' && value.includes('\n') ? (
<textarea
value={
value === null ? 'null' :
value === undefined ? '' :
value.toString()
}
onChange={(e) => updateValue(e.target.value, path)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0 resize-y"
placeholder="Long text value"
rows={3}
/>
) : (
<input
type="text"
value={
value === null ? 'null' :
value === undefined ? '' :
value.toString()
}
onChange={(e) => updateValue(e.target.value, path)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
placeholder="Value"
/>
)
<div className="flex-1 flex items-center gap-2">
{readOnly ? (
typeof value === 'string' && detectNestedData(value) ? (
<span
onClick={() => openNestedEditor(value, path)}
className="px-2 py-1 text-sm text-blue-600 dark:text-blue-400 font-mono break-all cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded transition-colors"
title={`Click to view nested ${detectNestedData(value).type} data`}
>
{getDisplayValue(value)}
</span>
) : (
<span className="px-2 py-1 text-sm text-gray-900 dark:text-gray-100 font-mono break-all">
{getDisplayValue(value)}
</span>
)
) : (
<>
{(fieldTypes[path] === 'longtext' || (typeof value === 'string' && value.includes('\n'))) ? (
<textarea
value={getDisplayValue(value)}
onChange={(e) => updateValue(e.target.value, path)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0 resize-y items"
placeholder="Long text value"
rows={3}
/>
) : (
<input
type="text"
value={getDisplayValue(value)}
onChange={(e) => updateValue(e.target.value, path)}
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
placeholder="Value"
/>
)}
{typeof value === 'string' && detectNestedData(value) && (
<button
onClick={() => openNestedEditor(value, path)}
className="p-1 text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded flex-shrink-0"
title={`Edit nested ${detectNestedData(value).type} data`}
>
<Edit3 className="h-4 w-4" />
</button>
)}
</>
)}
</div>
)
) : (
<span className="flex-1 text-sm text-gray-600 dark:text-gray-400">
@@ -414,36 +672,40 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
</span>
)}
<div className="flex items-center space-x-2 sm:space-x-2">
<select
value={
value === null ? 'null' :
value === undefined ? 'string' :
typeof value === 'string' ? (value.includes('\n') ? 'longtext' : 'string') :
typeof value === 'number' ? 'number' :
typeof value === 'boolean' ? 'boolean' :
Array.isArray(value) ? 'array' : 'object'
}
onChange={(e) => changeType(e.target.value, path)}
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
>
<option value="string">String</option>
<option value="longtext">Long Text</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="array">Array</option>
<option value="object">Object</option>
<option value="null">Null</option>
</select>
<button
onClick={() => removeProperty(key, parentPath)}
className="p-1 text-red-600 hover:bg-red-100 dark:hover:bg-red-900 rounded flex-shrink-0"
title="Remove property"
>
<Minus className="h-4 w-4" />
</button>
</div>
{!readOnly && (
<div className="flex items-center space-x-2 sm:space-x-2">
<select
value={
fieldTypes[path] || (
value === null ? 'null' :
value === undefined ? 'string' :
typeof value === 'string' ? (value.includes('\n') ? 'longtext' : 'string') :
typeof value === 'number' ? 'number' :
typeof value === 'boolean' ? 'boolean' :
Array.isArray(value) ? 'array' : 'object'
)
}
onChange={(e) => changeType(e.target.value, path)}
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
>
<option value="string">String</option>
<option value="longtext">Long Text</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="array">Array</option>
<option value="object">Object</option>
<option value="null">Null</option>
</select>
<button
onClick={() => removeProperty(key, parentPath)}
className="p-1 text-red-600 hover:bg-red-100 dark:hover:bg-red-900 rounded flex-shrink-0"
title="Remove property"
>
<Minus className="h-4 w-4" />
</button>
</div>
)}
</div>
</div>
@@ -454,26 +716,30 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
{value.map((item, index) =>
renderValue(item, index.toString(), `${path}.${index}`, path)
)}
<button
onClick={() => addArrayItem(value, path)}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
>
<Plus className="h-4 w-4" />
<span>Add Item</span>
</button>
{!readOnly && (
<button
onClick={() => addArrayItem(value, path)}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
>
<Plus className="h-4 w-4" />
<span>Add Item</span>
</button>
)}
</>
) : (
<>
{Object.entries(value).map(([k, v]) =>
renderValue(v, k, `${path}.${k}`, path)
)}
<button
onClick={() => addProperty(value, path)}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
>
<Plus className="h-4 w-4" />
<span>Add Property</span>
</button>
{!readOnly && (
<button
onClick={() => addProperty(value, path)}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
>
<Plus className="h-4 w-4" />
<span>Add Property</span>
</button>
)}
</>
)}
</div>
@@ -483,31 +749,117 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
};
return (
<div className="border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-white dark:bg-gray-800 min-h-96 w-full">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
<button
onClick={() => addProperty(data, 'root')}
className="flex items-center space-x-1 px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex-shrink-0"
>
<Plus className="h-4 w-4" />
<span className="hidden sm:inline">Add Property</span>
<span className="sm:hidden">Add</span>
</button>
<div className="min-h-96 w-full">
<div className="mb-4">
<div className="flex flex-col gap-3 mb-3">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
{/* Mode Toggle - Below title on mobile, inline on desktop */}
{readOnlyProp === false && (
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm w-fit">
<button
onClick={() => setEditMode(false)}
className={`flex items-center gap-2 px-3 py-1.5 text-xs font-medium transition-colors ${
!editMode
? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
<Eye className="h-3.5 w-3.5" />
<span>Preview</span>
</button>
<button
onClick={() => setEditMode(true)}
className={`flex items-center gap-2 px-3 py-1.5 text-xs font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${
editMode
? 'bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-300'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
<Pencil className="h-3.5 w-3.5" />
<span>Edit</span>
</button>
</div>
)}
</div>
</div>
<div className="overflow-x-hidden">
{Object.keys(data).length === 0 ? (
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
<Braces className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No properties yet. Click "Add Property" to start building your data structure.</p>
<div className="w-full overflow-hidden">
<div className="w-full overflow-x-auto">
<div className="min-w-max">
{Object.keys(data).length === 0 ? (
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
<Braces className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No properties yet. Click "Add Property" to start building your data structure.</p>
</div>
) : (
Object.entries(data).map(([key, value]) =>
renderValue(value, key, `root.${key}`, 'root')
)
)}
{/* Root level Add Property button */}
{!readOnly && (
<button
onClick={() => addProperty(data, 'root')}
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
>
<Plus className="h-4 w-4" />
<span>Add Property</span>
</button>
)}
</div>
) : (
Object.entries(data).map(([key, value]) =>
renderValue(value, key, `root.${key}`, 'root')
)
)}
</div>
</div>
{/* Nested Data Editor Modal */}
{nestedEditModal && nestedData && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[99999] p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Modal Header */}
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-blue-50 dark:bg-blue-900/20 flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Edit Nested {nestedEditModal.type === 'json' ? 'JSON' : 'Serialized'} Data
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Changes will be saved back as a {nestedEditModal.type === 'json' ? 'JSON' : 'serialized'} string
</p>
</div>
<button
onClick={closeNestedEditor}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded self-start"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Modal Body - Nested Editor */}
<div className="flex-1 overflow-auto p-6">
<StructuredEditor
initialData={nestedData}
onDataChange={setNestedData}
/>
</div>
{/* Modal Footer */}
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/20 flex justify-end gap-3">
<button
onClick={closeNestedEditor}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
>
Cancel
</button>
<button
onClick={saveNestedEdit}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 rounded-md transition-colors"
>
Save Changes
</button>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -1,28 +1,97 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
import { getCategoryConfig } from '../config/tools';
const ToolCard = ({ icon: Icon, title, description, path, tags, category }) => {
const categoryConfig = getCategoryConfig(category);
// Define explicit hover classes for Tailwind CSS purging
const getHoverClasses = (category) => {
switch (category) {
case 'editor':
return {
border: 'hover:border-blue-300 dark:hover:border-blue-500',
shadow: 'hover:shadow-blue-500/20',
titleColor: 'group-hover:text-blue-600 dark:group-hover:text-blue-400',
arrowColor: 'group-hover:text-blue-600',
badgeColor: 'group-hover:bg-blue-100 dark:group-hover:bg-blue-900/30 group-hover:text-blue-700 dark:group-hover:text-blue-300'
};
case 'encoder':
return {
border: 'hover:border-purple-300 dark:hover:border-purple-500',
shadow: 'hover:shadow-purple-500/20',
titleColor: 'group-hover:text-purple-600 dark:group-hover:text-purple-400',
arrowColor: 'group-hover:text-purple-600',
badgeColor: 'group-hover:bg-purple-100 dark:group-hover:bg-purple-900/30 group-hover:text-purple-700 dark:group-hover:text-purple-300'
};
case 'formatter':
return {
border: 'hover:border-green-300 dark:hover:border-green-500',
shadow: 'hover:shadow-green-500/20',
titleColor: 'group-hover:text-green-600 dark:group-hover:text-green-400',
arrowColor: 'group-hover:text-green-600',
badgeColor: 'group-hover:bg-green-100 dark:group-hover:bg-green-900/30 group-hover:text-green-700 dark:group-hover:text-green-300'
};
case 'analyzer':
return {
border: 'hover:border-orange-300 dark:hover:border-orange-500',
shadow: 'hover:shadow-orange-500/20',
titleColor: 'group-hover:text-orange-600 dark:group-hover:text-orange-400',
arrowColor: 'group-hover:text-orange-600',
badgeColor: 'group-hover:bg-orange-100 dark:group-hover:bg-orange-900/30 group-hover:text-orange-700 dark:group-hover:text-orange-300'
};
default:
return {
border: 'hover:border-slate-300 dark:hover:border-slate-500',
shadow: 'hover:shadow-slate-500/20',
titleColor: 'group-hover:text-slate-600 dark:group-hover:text-slate-400',
arrowColor: 'group-hover:text-slate-600',
badgeColor: 'group-hover:bg-slate-100 dark:group-hover:bg-slate-700 group-hover:text-slate-700 dark:group-hover:text-slate-300'
};
}
};
const hoverClasses = getHoverClasses(category);
const ToolCard = ({ icon: Icon, title, description, path, tags }) => {
return (
<Link to={path} className="block">
<div className="tool-card group cursor-pointer">
<div className="flex items-start space-x-4">
<div className="flex-shrink-0">
<Icon className="h-8 w-8 text-primary-600" />
<Link to={path} className="block group">
<div className={`relative overflow-hidden rounded-2xl bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm border border-slate-200 dark:border-slate-700 ${hoverClasses.border} transition-all duration-300 hover:shadow-2xl ${hoverClasses.shadow} hover:-translate-y-1`}>
{/* Gradient overlay on hover */}
<div className={`absolute inset-0 bg-gradient-to-br ${categoryConfig.color} opacity-0 group-hover:opacity-5 transition-opacity duration-300`}></div>
<div className="relative p-6">
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className={`flex-shrink-0 p-3 rounded-xl bg-gradient-to-br ${categoryConfig.color} shadow-lg group-hover:scale-110 transition-transform duration-300`}>
<Icon className="h-6 w-6 text-white" />
</div>
<div className="flex-shrink-0 ml-4">
<ArrowRight className={`h-5 w-5 text-slate-400 ${hoverClasses.arrowColor} group-hover:translate-x-1 transition-all duration-300`} />
</div>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white group-hover:text-primary-600 transition-colors">
{title}
</h3>
<p className="text-gray-600 dark:text-gray-300 mt-1">
{/* Content */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<h3 className={`text-xl font-bold text-slate-800 dark:text-white ${hoverClasses.titleColor} transition-colors`}>
{title}
</h3>
<span className={`px-2 py-1 text-xs font-medium bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 rounded-full ${hoverClasses.badgeColor} transition-colors`}>
{categoryConfig.name}
</span>
</div>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed group-hover:text-slate-700 dark:group-hover:text-slate-200 transition-colors">
{description}
</p>
{tags && (
<div className="flex flex-wrap gap-2 mt-3">
{tags && tags.length > 0 && (
<div className="flex flex-wrap gap-2 pt-2">
{tags.map((tag, index) => (
<span
key={index}
className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full"
className="px-3 py-1 text-xs font-medium bg-slate-50 dark:bg-slate-700/50 text-slate-500 dark:text-slate-400 rounded-full border border-slate-200 dark:border-slate-600 group-hover:border-slate-300 dark:group-hover:border-slate-500 transition-colors"
>
{tag}
</span>
@@ -30,9 +99,6 @@ const ToolCard = ({ icon: Icon, title, description, path, tags }) => {
</div>
)}
</div>
<div className="flex-shrink-0">
<ArrowRight className="h-5 w-5 text-gray-400 group-hover:text-primary-600 transition-colors" />
</div>
</div>
</div>
</Link>

View File

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

View File

@@ -1,96 +1,409 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Search, LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react';
import React, { useState, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { Search, ChevronLeft, ChevronRight, Sparkles, ChevronDown, ChevronUp } from 'lucide-react';
import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
import useNavigationGuard from '../hooks/useNavigationGuard';
const ToolSidebar = () => {
const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
const location = useLocation();
const { navigateWithGuard: hookNavigateWithGuard } = useNavigationGuard();
// Use prop navigation guard if provided, otherwise use hook
const navigateWithGuard = propNavigateWithGuard || hookNavigateWithGuard;
const [isCollapsed, setIsCollapsed] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [expandedCategories, setExpandedCategories] = useState({
editor: false,
encoder: false,
formatter: false,
analyzer: false
});
const [hoveredTooltip, setHoveredTooltip] = useState(null);
const tooltipTimeoutRef = useRef(null);
const tools = [
{ path: '/', name: 'Home', icon: Home, description: 'Back to homepage' },
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
{ path: '/beautifier', name: 'Beautifier Tool', icon: Wand2, description: 'Beautify/minify code' },
{ path: '/diff', name: 'Diff Tool', icon: GitCompare, description: 'Compare text differences' },
{ path: '/text-length', name: 'Text Length Checker', icon: Type, description: 'Analyze text length & stats' },
];
const filteredTools = tools.filter(tool =>
// Filter non-tools and tools separately
const filteredNonTools = NON_TOOLS.filter(tool =>
tool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.description.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredTools = TOOLS.filter(tool =>
tool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.description.toLowerCase().includes(searchTerm.toLowerCase())
);
const isActive = (path) => location.pathname === path;
// Toggle category expansion - close others when opening one
const toggleCategory = (categoryKey) => {
setExpandedCategories(prev => {
const isCurrentlyExpanded = prev[categoryKey];
if (isCurrentlyExpanded) {
// If currently expanded, just close it
return {
...prev,
[categoryKey]: false
};
} else {
// If currently closed, close all others and open this one
const newState = {
editor: false,
encoder: false,
formatter: false,
analyzer: false
};
newState[categoryKey] = true;
return newState;
}
});
};
// Tooltip hover handlers
const handleTooltipMouseEnter = (categoryKey) => {
if (tooltipTimeoutRef.current) {
clearTimeout(tooltipTimeoutRef.current);
}
setHoveredTooltip(categoryKey);
};
const handleTooltipMouseLeave = () => {
tooltipTimeoutRef.current = setTimeout(() => {
setHoveredTooltip(null);
}, 300); // 300ms delay before hiding
};
// Handle navigation with data validation
const handleNavigation = (path, event) => {
event.preventDefault();
navigateWithGuard(path);
};
// Group tools by category
const toolsByCategory = {};
filteredTools.forEach(tool => {
if (!toolsByCategory[tool.category]) {
toolsByCategory[tool.category] = [];
}
toolsByCategory[tool.category].push(tool);
});
return (
<div className={`bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 transition-all duration-300 sticky top-16 ${
<div className={`bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm border-r border-slate-200/50 dark:border-slate-700/50 transition-all duration-300 ${
isCollapsed ? 'w-16' : 'w-64'
}`} style={{ height: 'calc(100vh - 4rem)' }}>
} sticky top-16 ${isCollapsed ? 'overflow-visible' : 'overflow-hidden'}`} style={{ height: 'calc(100vh - 4rem)' }}>
<div className="h-full flex flex-col">
{/* Sidebar Header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="p-4 border-b border-slate-200/50 dark:border-slate-700/50">
<div className="flex items-center justify-between">
{!isCollapsed && (
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Tools
</h2>
<div className="flex items-center gap-2">
<Sparkles className="h-4 w-4 text-blue-500" />
<h2 className="text-lg font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
Tools
</h2>
</div>
)}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
className="p-2 rounded-xl hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 group"
>
{isCollapsed ? (
<ChevronRight className="h-4 w-4 text-gray-500" />
<ChevronRight className="h-4 w-4 text-slate-500 group-hover:text-blue-500 transition-colors" />
) : (
<ChevronLeft className="h-4 w-4 text-gray-500" />
<ChevronLeft className="h-4 w-4 text-slate-500 group-hover:text-blue-500 transition-colors" />
)}
</button>
</div>
{/* Search - only show when not collapsed */}
{!isCollapsed && (
<div className="relative mt-3">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Search tools..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
<div className="relative mt-4">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/10 to-purple-500/10 rounded-xl blur opacity-50"></div>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-400" />
<input
type="text"
placeholder="Search tools..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-9 pr-3 py-2.5 text-sm border border-slate-200 dark:border-slate-600 rounded-xl bg-white/80 dark:bg-slate-700/80 backdrop-blur-sm text-slate-900 dark:text-slate-100 placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300"
/>
</div>
</div>
)}
</div>
{/* Tools List */}
<div className="flex-1 overflow-y-auto py-2">
<nav className="space-y-1 px-2">
{filteredTools.map((tool) => {
<div className={`flex-1 py-3 ${isCollapsed ? 'overflow-visible' : 'overflow-y-auto'}`}>
<nav className="space-y-2 px-3">
{/* Render Non-Tools (Home, What's New) with special styling */}
{filteredNonTools.map((tool) => {
const IconComponent = tool.icon;
const isActiveItem = isActive(tool.path);
const isWhatsNew = tool.path === '/release-notes';
return (
<Link
<a
key={tool.path}
to={tool.path}
className={`group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors ${
isActive(tool.path)
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-700'
href={tool.path}
onClick={(e) => handleNavigation(tool.path, e)}
className={`group flex items-center text-sm font-medium rounded-xl transition-all duration-300 cursor-pointer ${
isActiveItem
? isCollapsed
? ' justify-center py-3'
: isWhatsNew
? 'bg-gradient-to-r from-amber-50 to-yellow-50 dark:from-amber-900/30 dark:to-yellow-800/30 shadow-lg px-3 py-3 border-2 border-amber-200 dark:border-amber-700'
: 'bg-gradient-to-r from-indigo-50 to-indigo-100 dark:from-indigo-900/30 dark:to-indigo-800/30 shadow-lg px-3 py-3'
: isCollapsed
? ' justify-center py-3'
: isWhatsNew
? 'hover:bg-gradient-to-r hover:from-amber-50 hover:to-yellow-50 dark:hover:from-amber-900/20 dark:hover:to-yellow-800/20 px-3 py-3 border border-amber-200/50 dark:border-amber-700/50'
: 'hover:bg-white/50 dark:hover:bg-slate-700/50 px-3 py-3'
}`}
title={isCollapsed ? tool.name : ''}
>
<IconComponent className={`h-5 w-5 ${isCollapsed ? '' : 'mr-3'} flex-shrink-0`} />
{!isCollapsed && (
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{tool.name}</div>
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">
{tool.description}
{isCollapsed ? (
<div className={`rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 ${
isActiveItem
? isWhatsNew
? 'bg-gradient-to-br from-amber-500 to-yellow-500 p-3'
: 'bg-gradient-to-br from-indigo-500 to-purple-500 p-3'
: isWhatsNew
? 'border-2 border-amber-300 dark:border-amber-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-amber-500 group-hover:to-yellow-500 p-2'
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500 p-2'
}`}>
<IconComponent className={`${
isActiveItem
? 'h-5 w-5 text-white'
: 'h-4 w-4 text-slate-500 dark:text-slate-400 group-hover:text-white'
}`} />
</div>
) : (
<>
<div className={`p-2 rounded-lg shadow-sm group-hover:scale-110 transition-transform duration-300 mr-3 flex-shrink-0 ${
isActiveItem
? isWhatsNew
? 'bg-gradient-to-br from-amber-500 to-yellow-500'
: 'bg-gradient-to-br from-indigo-500 to-purple-500'
: isWhatsNew
? 'border-2 border-amber-300 dark:border-amber-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-amber-500 group-hover:to-yellow-500'
: 'border-2 border-indigo-300 dark:border-indigo-600 bg-transparent group-hover:bg-gradient-to-br group-hover:from-indigo-500 group-hover:to-purple-500'
}`}>
<IconComponent className={`h-4 w-4 ${
isActiveItem
? 'text-white'
: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium truncate ${
isActiveItem
? isWhatsNew
? 'text-amber-700 dark:text-amber-300'
: 'text-indigo-700 dark:text-indigo-300'
: isWhatsNew
? 'text-slate-500 dark:text-slate-400 group-hover:text-amber-600 dark:group-hover:text-amber-400'
: 'text-slate-500 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400'
}`}>
{tool.name}
{isWhatsNew && !isCollapsed && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300">
New
</span>
)}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 truncate">
{tool.description}
</div>
</div>
</>
)}
</a>
);
})}
{/* Separator between non-tools and tools */}
{!isCollapsed && filteredNonTools.length > 0 && Object.keys(toolsByCategory).length > 0 && (
<div className="border-t border-slate-200/50 dark:border-slate-700/50 my-4"></div>
)}
{/* Render Tools by Category */}
{!isCollapsed && Object.entries(toolsByCategory).map(([categoryKey, tools]) => {
const categoryConfig = getCategoryConfig(categoryKey);
const isExpanded = expandedCategories[categoryKey];
return (
<div key={categoryKey} className="mb-2">
{/* Category Header */}
<button
onClick={() => toggleCategory(categoryKey)}
className="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors rounded-lg hover:bg-white/50 dark:hover:bg-slate-700/50"
>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full bg-gradient-to-r ${categoryConfig.color}`}></div>
<span className="uppercase tracking-wider">{categoryConfig.name}</span>
<span className="text-slate-400 dark:text-slate-500">({tools.length})</span>
</div>
{isExpanded ? (
<ChevronUp className="h-3 w-3" />
) : (
<ChevronDown className="h-3 w-3" />
)}
</button>
{/* Category Tools */}
{isExpanded && (
<div className="ml-4 space-y-1 mt-1">
{tools.map((tool) => {
const IconComponent = tool.icon;
const isActiveItem = isActive(tool.path);
const getActiveClasses = (category) => {
switch (category) {
case 'editor':
return {
expanded: 'bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-900/30 dark:to-blue-800/30',
titleColor: 'text-blue-700 dark:text-blue-300',
iconBg: 'bg-gradient-to-br from-blue-500 to-cyan-500'
};
case 'encoder':
return {
expanded: 'bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/30 dark:to-purple-800/30',
titleColor: 'text-purple-700 dark:text-purple-300',
iconBg: 'bg-gradient-to-br from-purple-500 to-pink-500'
};
case 'formatter':
return {
expanded: 'bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/30 dark:to-green-800/30',
titleColor: 'text-green-700 dark:text-green-300',
iconBg: 'bg-gradient-to-br from-green-500 to-emerald-500'
};
case 'analyzer':
return {
expanded: 'bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-900/30 dark:to-orange-800/30',
titleColor: 'text-orange-700 dark:text-orange-300',
iconBg: 'bg-gradient-to-br from-orange-500 to-red-500'
};
default:
return {
expanded: 'bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-700 dark:to-slate-600',
titleColor: 'text-slate-700 dark:text-slate-300',
iconBg: 'bg-gradient-to-br from-slate-500 to-slate-600'
};
}
};
const activeClasses = getActiveClasses(tool.category);
return (
<a
key={tool.path}
href={tool.path}
onClick={(e) => handleNavigation(tool.path, e)}
className={`group flex items-center text-sm font-medium rounded-lg transition-all duration-300 px-3 py-2 cursor-pointer ${
isActiveItem
? activeClasses.expanded + ' shadow-md'
: 'hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<div className={`p-1.5 rounded-md shadow-sm group-hover:scale-110 transition-transform duration-300 mr-3 flex-shrink-0 ${
isActiveItem
? activeClasses.iconBg
: `border border-${categoryConfig.color.split('-')[1]}-300 dark:border-${categoryConfig.color.split('-')[1]}-600 bg-transparent group-hover:bg-gradient-to-br group-hover:${categoryConfig.color}`
}`}>
<IconComponent className={`h-3.5 w-3.5 ${
isActiveItem
? 'text-white'
: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium truncate text-sm ${
isActiveItem ? activeClasses.titleColor : 'text-slate-600 dark:text-slate-400 group-hover:text-slate-800 dark:group-hover:text-slate-200'
}`}>
{tool.name}
</div>
<div className="text-xs text-slate-500 dark:text-slate-500 truncate">
{tool.description}
</div>
</div>
</a>
);
})}
</div>
)}
</Link>
</div>
);
})}
{/* Collapsed view - show categories with tooltip submenus */}
{isCollapsed && Object.entries(toolsByCategory).map(([categoryKey, tools]) => {
const categoryConfig = getCategoryConfig(categoryKey);
const isTooltipVisible = hoveredTooltip === categoryKey;
return (
<div
key={categoryKey}
className="relative"
onMouseEnter={() => handleTooltipMouseEnter(categoryKey)}
onMouseLeave={handleTooltipMouseLeave}
>
<div className="flex items-center justify-center py-3 rounded-xl transition-all duration-300 cursor-pointer">
<div className={`rounded-lg shadow-sm hover:scale-110 transition-transform duration-300 p-2 bg-gradient-to-br ${categoryConfig.color} ${isTooltipVisible ? 'opacity-100 scale-110' : 'opacity-80 hover:opacity-100'}`}>
<div className="h-4 w-4 bg-white rounded-sm flex items-center justify-center">
<span className="text-xs font-bold text-gray-700">{tools.length}</span>
</div>
</div>
</div>
{/* Tooltip Submenu */}
<div className={`absolute left-full ml-2 top-0 w-64 bg-white dark:bg-slate-800 rounded-lg shadow-xl border border-gray-200 dark:border-slate-700 z-[9999] transition-all duration-200 transform ${
isTooltipVisible
? 'opacity-100 visible translate-x-0 pointer-events-auto'
: 'opacity-0 invisible translate-x-2 pointer-events-none'
}`}>
<div className="p-3">
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-gray-200 dark:border-slate-700">
<div className={`w-3 h-3 rounded-full bg-gradient-to-r ${categoryConfig.color}`}></div>
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100 uppercase tracking-wider">{categoryConfig.name}</span>
</div>
<div className="space-y-1">
{tools.map((tool) => {
const IconComponent = tool.icon;
const isActiveItem = isActive(tool.path);
return (
<a
key={tool.path}
href={tool.path}
onClick={(e) => handleNavigation(tool.path, e)}
className={`flex items-center gap-3 p-2 rounded-lg transition-colors cursor-pointer ${
isActiveItem
? `bg-gradient-to-r ${categoryConfig.color.replace('from-', 'from-').replace('to-', 'to-')}20 text-gray-900 dark:text-gray-100`
: 'hover:bg-gray-50 dark:hover:bg-slate-700 text-gray-700 dark:text-gray-300'
}`}
>
<div className={`p-1.5 rounded-md ${
isActiveItem
? `bg-gradient-to-br ${categoryConfig.color}`
: `border border-gray-300 dark:border-slate-600 bg-transparent`
}`}>
<IconComponent className={`h-3.5 w-3.5 ${
isActiveItem ? 'text-white' : 'text-gray-500 dark:text-gray-400'
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium text-sm truncate ${isActiveItem ? `text-white` : `text-gray-500 dark:text-gray-400`}`}>{tool.name}</div>
<div className={`text-xs ${isActiveItem ? `text-white` : `text-gray-500 dark:text-gray-400`} truncate`}>{tool.description}</div>
</div>
</a>
);
})}
</div>
</div>
</div>
</div>
);
})}
</nav>
@@ -98,9 +411,18 @@ const ToolSidebar = () => {
{/* Footer */}
{!isCollapsed && (
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
<div className="text-xs text-gray-500 dark:text-gray-400 text-center">
Quick access to all tools
<div className="p-4 border-t border-slate-200/50 dark:border-slate-700/50">
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<div className="w-1.5 h-1.5 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full animate-pulse"></div>
<span className="text-xs font-medium text-slate-500 dark:text-slate-400">
Quick Access
</span>
<div className="w-1.5 h-1.5 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-xs text-slate-400 dark:text-slate-500">
{SITE_CONFIG.totalTools} tools available
</p>
</div>
</div>
)}

View File

@@ -0,0 +1,703 @@
import React from 'react';
const MinimalTemplate = ({ invoiceData, formatNumber, formatCurrency }) => {
// Get the chosen color scheme or default to golden
const accentColor = invoiceData.settings?.colorScheme || '#D4AF37';
// Get layout settings
const sectionSpacing = invoiceData.settings?.sectionSpacing || 'normal';
const pageBreaks = invoiceData.settings?.pageBreaks || {};
// Section spacing values
const spacingMap = {
compact: '15px',
normal: '25px',
spacious: '40px'
};
return (
<div style={{
padding: '10px', // Further reduced for better print layout
fontSize: '13px',
lineHeight: '1.4',
fontFamily: 'system-ui, -apple-system, sans-serif',
color: '#000000',
position: 'relative'
}}>
{/* Header Section - 2 Column Layout */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px',
padding: '20px',
background: `linear-gradient(135deg, ${accentColor}, ${accentColor}dd)`,
borderRadius: '4px',
color: 'white'
}}>
{/* Left: Logo + INVOICE + Company Name */}
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
{/* Company Logo */}
{invoiceData.company.logo ? (
<img
src={invoiceData.company.logo}
alt="Company Logo"
style={{
width: '60px',
height: '60px',
objectFit: 'contain',
borderRadius: '8px',
background: 'white',
padding: '4px'
}}
/>
) : (
<div style={{
width: '60px',
height: '60px',
background: 'rgba(255,255,255,0.2)',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
color: 'white',
fontWeight: 'bold'
}}>
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<h2 style={{
fontSize: '32px',
fontWeight: 'bold',
color: 'white',
margin: '0',
lineHeight: '1'
}}>
INVOICE
</h2>
<div style={{
fontSize: '16px',
fontWeight: '600',
color: 'rgba(255,255,255,0.9)',
margin: '4px 0 0 0'
}}>
{invoiceData.company.name || 'DevTools Inc.'}
</div>
</div>
</div>
{/* Right: Invoice Details */}
<div style={{ textAlign: 'right' }}>
<div style={{ marginBottom: '8px', display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '12px' }}>
<span style={{ fontSize: '16px', fontWeight: '600', color: 'rgba(255,255,255,0.8)' }}>#</span>
<span style={{ fontSize: '18px', fontWeight: 'bold', color: 'white' }}>
{invoiceData.invoiceNumber || 'INV-2024-001'}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '12px' }}>
<span style={{ fontSize: '16px', fontWeight: '600', color: 'rgba(255,255,255,0.8)' }}>Date:</span>
<span style={{ fontSize: '16px', fontWeight: '600', color: 'white' }}>
{invoiceData.date || '15/01/2024'}
</span>
</div>
{invoiceData.dueDate && (
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', gap: '12px', marginTop: '4px' }}>
<span style={{ fontSize: '16px', fontWeight: '600', color: 'rgba(255,255,255,0.8)' }}>Due:</span>
<span style={{ fontSize: '16px', fontWeight: '600', color: 'white' }}>
{invoiceData.dueDate}
</span>
</div>
)}
</div>
</div>
{/* Subheader Section - 2 Column Layout: FROM and TO */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '30px', marginBottom: '25px' }}>
{/* FROM Section */}
{(invoiceData.settings?.showFromSection ?? true) && (
<div className={'invoice-from-to-card'} style={{
padding: '20px',
background: `${accentColor}08`,
borderRadius: '4px',
border: '1px solid #e9ecef'
}}>
<h3 style={{
fontSize: '14px',
fontWeight: 'bold',
color: accentColor,
marginBottom: '12px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
FROM
</h3>
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
{invoiceData.company.name || 'DevTools Inc.'}
</div>
{invoiceData.company.address && <div>{invoiceData.company.address}</div>}
{invoiceData.company.city && <div>{invoiceData.company.city}</div>}
{invoiceData.company.phone && <div>{invoiceData.company.phone}</div>}
{invoiceData.company.email && <div>{invoiceData.company.email}</div>}
</div>
</div>
)}
{/* TO Section */}
<div className={'invoice-from-to-card'} style={{
padding: '20px',
background: `${accentColor}08`,
borderRadius: '4px',
border: '1px solid #e9ecef'
}}>
<h3 style={{
fontSize: '14px',
fontWeight: 'bold',
color: accentColor,
marginBottom: '12px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
TO
</h3>
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
{invoiceData.client.name || 'Acme Corporation'}
</div>
{invoiceData.client.address && <div>{invoiceData.client.address}</div>}
{invoiceData.client.city && <div>{invoiceData.client.city}</div>}
{invoiceData.client.phone && <div>{invoiceData.client.phone}</div>}
{invoiceData.client.email && <div>{invoiceData.client.email}</div>}
</div>
</div>
</div>
{/* Payment Terms Section */}
{invoiceData.paymentTerms?.type !== 'full' && (
<div
className={pageBreaks.beforePaymentSchedule ? 'page-break-before' : ''}
style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }}
>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
marginBottom: '16px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
Payment Schedule
</h3>
<div style={{
background: `${accentColor}08`,
borderRadius: '4px',
border: '1px solid #e9ecef',
padding: '16px'
}}>
{/* Down Payment */}
{invoiceData.paymentTerms?.type === 'downpayment' && invoiceData.paymentTerms?.downPayment?.amount > 0 && (
<div style={{ marginBottom: '12px', paddingBottom: '12px', borderBottom: '1px solid #e9ecef' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#000000', marginBottom: '4px' }}>
Down Payment ({invoiceData.paymentTerms.downPayment.percentage?.toFixed(1)}%)
</div>
<div style={{ fontSize: '12px', marginTop: '2px' }}>
{invoiceData.paymentTerms.downPayment.status === 'overdue' && invoiceData.paymentTerms.downPayment.dueDate && (
<span style={{ color: '#EF4444' }}>
Overdue - Due: {new Date(invoiceData.paymentTerms.downPayment.dueDate).toLocaleDateString()}
</span>
)}
{invoiceData.paymentTerms.downPayment.status === 'current' && invoiceData.paymentTerms.downPayment.dueDate && (
<span style={{ color: accentColor }}>
Current - <span style={{ color: '#666666' }}>Due: {new Date(invoiceData.paymentTerms.downPayment.dueDate).toLocaleDateString()}</span>
</span>
)}
{invoiceData.paymentTerms.downPayment.status === 'paid' && (
<span style={{ color: '#666666' }}>Paid</span>
)}
{(!invoiceData.paymentTerms.downPayment.status || invoiceData.paymentTerms.downPayment.status === 'pending') && (
<span style={{ color: '#666666' }}>Pending</span>
)}
</div>
</div>
</div>
<span style={{
fontSize: '14px',
fontWeight: 'bold',
color: invoiceData.paymentTerms.downPayment.status === 'current' ? accentColor : '#666666'
}}>
{new Intl.NumberFormat('en-US', {
style: 'currency',
currency: invoiceData.settings?.currency?.code || 'USD'
}).format(invoiceData.paymentTerms.downPayment.amount)}
</span>
</div>
</div>
)}
{/* Installments */}
{invoiceData.paymentTerms?.installments && invoiceData.paymentTerms.installments.length > 0 && (
<div>
{invoiceData.paymentTerms?.type === 'downpayment' && (
<div style={{ fontSize: '12px', color: '#666666', marginBottom: '8px' }}>
Remaining Balance: {new Intl.NumberFormat('en-US', {
style: 'currency',
currency: invoiceData.settings?.currency?.code || 'USD'
}).format(invoiceData.total - (invoiceData.paymentTerms?.downPayment?.amount || 0))}
</div>
)}
{invoiceData.paymentTerms.installments.map((installment, index) => (
<div key={installment.id} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '8px',
padding: '8px',
background: 'rgba(255,255,255,0.5)',
borderRadius: '4px'
}}>
<div>
<div>
<div style={{ fontSize: '14px', fontWeight: '500', color: '#000000', marginBottom: '4px' }}>
{installment.description || `Installment ${index + 1}`}
</div>
<div style={{ fontSize: '12px', marginTop: '2px' }}>
{installment.status === 'overdue' && installment.dueDate && (
<span style={{ color: '#EF4444' }}>
Overdue - Due: {new Date(installment.dueDate).toLocaleDateString()}
</span>
)}
{installment.status === 'current' && installment.dueDate && (
<span style={{ color: accentColor }}>
Current - <span style={{ color: '#666666' }}>Due: {new Date(installment.dueDate).toLocaleDateString()}</span>
</span>
)}
{installment.status === 'paid' && (
<span style={{ color: '#666666' }}>Paid</span>
)}
{(!installment.status || installment.status === 'pending') && (
<span style={{ color: '#666666' }}>Pending</span>
)}
</div>
</div>
</div>
<span style={{
fontSize: '14px',
fontWeight: 'bold',
color: installment.status === 'current' ? accentColor : '#666666'
}}>
{new Intl.NumberFormat('en-US', {
style: 'currency',
currency: invoiceData.settings?.currency?.code || 'USD'
}).format(installment.amount || 0)}
</span>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* Items Table */}
<div
className={pageBreaks.beforeItemsTable ? 'page-break-before' : ''}
style={{ marginBottom: '20px', marginTop: spacingMap[sectionSpacing] }}
>
<table className="invoice-table" style={{ width: '100%', borderCollapse: 'collapse' }}>
{/* Table Header */}
<thead>
<tr style={{
borderBottom: `2px solid ${accentColor}`,
backgroundColor: `${accentColor}15` // 15 = ~8% opacity
}}>
<th style={{
padding: '12px 0px 12px 16px',
textAlign: 'left',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle',
height: '48px'
}}>Item</th>
<th style={{
padding: '12px 0px',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle',
height: '48px'
}}>Quantity</th>
<th style={{
padding: '12px 0px',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle',
height: '48px'
}}>Unit Price</th>
<th style={{
padding: '12px 16px 12px 0px',
textAlign: 'right',
fontSize: '16px',
fontWeight: 'bold',
color: accentColor,
verticalAlign: 'middle',
height: '48px'
}}>Total</th>
</tr>
</thead>
{/* Table Body */}
<tbody>
{invoiceData.items.map((item, index) => (
<tr key={item.id} style={{
borderBottom: '1px solid #E5E5E5'
}}>
<td style={{
padding: '16px 0px 16px 16px',
fontSize: '14px',
color: '#000000',
verticalAlign: 'middle',
height: '52px'
}}>
{item.description}
</td>
<td style={{
padding: '16px 0px',
textAlign: 'center',
fontSize: '14px',
color: '#000000',
verticalAlign: 'middle',
height: '52px'
}}>
{item.quantity}
</td>
<td style={{
padding: '16px 0px',
textAlign: 'right',
fontSize: '14px',
color: '#000000',
verticalAlign: 'middle',
height: '52px'
}}>
{formatCurrency(item.rate, true)}
</td>
<td style={{
padding: '16px 16px 16px 0px',
textAlign: 'right',
fontSize: '14px',
color: '#000000',
fontWeight: 'bold',
verticalAlign: 'middle',
height: '52px'
}}>
{formatCurrency(item.amount, true)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Payment Method and Totals */}
<div
className={pageBreaks.beforePaymentMethod ? 'page-break-before' : ''}
style={{ display: 'flex', justifyContent: 'space-between', gap: '40px', marginBottom: '40px', marginTop: spacingMap[sectionSpacing] }}
>
{/* Payment Method */}
{invoiceData.paymentMethod?.type !== 'none' && (
<div style={{ flex: 1 }}>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#000000',
marginBottom: '12px'
}}>
Payment Method
</h3>
{/* Bank Details */}
{invoiceData.paymentMethod?.type === 'bank' && (
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
{invoiceData.paymentMethod.bankDetails?.bankName && (
<div style={{ marginBottom: '4px' }}>{invoiceData.paymentMethod.bankDetails.bankName}</div>
)}
{invoiceData.paymentMethod.bankDetails?.accountName && (
<div style={{ marginBottom: '4px' }}>Account Name: {invoiceData.paymentMethod.bankDetails.accountName}</div>
)}
{invoiceData.paymentMethod.bankDetails?.accountNumber && (
<div style={{ marginBottom: '4px' }}>Account No.: {invoiceData.paymentMethod.bankDetails.accountNumber}</div>
)}
{invoiceData.paymentMethod.bankDetails?.routingNumber && (
<div style={{ marginBottom: '4px' }}>Routing: {invoiceData.paymentMethod.bankDetails.routingNumber}</div>
)}
{invoiceData.paymentMethod.bankDetails?.swiftCode && (
<div style={{ marginBottom: '4px' }}>SWIFT: {invoiceData.paymentMethod.bankDetails.swiftCode}</div>
)}
{invoiceData.paymentMethod.bankDetails?.iban && (
<div style={{ marginBottom: '4px' }}>IBAN: {invoiceData.paymentMethod.bankDetails.iban}</div>
)}
{invoiceData.dueDate && (
<div>
Pay by: {invoiceData.dueDate}
</div>
)}
</div>
)}
{/* Payment Link */}
{invoiceData.paymentMethod?.type === 'link' && invoiceData.paymentMethod.paymentLink?.url && (
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<a
href={invoiceData.paymentMethod.paymentLink.url}
target="_blank"
rel="noopener noreferrer"
style={{
display: 'inline-block',
padding: '8px 16px',
background: accentColor,
color: 'white',
textDecoration: 'none',
borderRadius: '4px',
fontSize: '14px',
fontWeight: '600'
}}
>
{invoiceData.paymentMethod.paymentLink.label || 'Pay Online'}
</a>
{invoiceData.dueDate && <div style={{ marginTop: '8px' }}>Pay by: {invoiceData.dueDate}</div>}
</div>
)}
{/* QR Code */}
{invoiceData.paymentMethod?.type === 'qr' && (invoiceData.paymentMethod.qrCode?.url || invoiceData.paymentMethod.qrCode?.customImage) && (
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div style={{ marginBottom: '8px' }}>
<img
src={
invoiceData.paymentMethod.qrCode.customImage
? invoiceData.paymentMethod.qrCode.customImage
: `https://api.qrserver.com/v1/create-qr-code/?size=80x80&data=${encodeURIComponent(invoiceData.paymentMethod.qrCode.url)}`
}
alt="Payment QR Code"
style={{ width: '80px', height: '80px', border: '1px solid #e9ecef' }}
/>
</div>
<div style={{ fontSize: '12px', color: '#666666', marginBottom: '4px' }}>
{invoiceData.paymentMethod.qrCode.label || 'Scan to Pay'}
</div>
{invoiceData.dueDate && (
<div>
Pay by: {invoiceData.dueDate}
{invoiceData.settings?.paymentStatus === 'PAID' && invoiceData.settings?.paymentDate && (
<div style={{ color: '#22c55e', fontWeight: 'bold', marginTop: '4px' }}>
Paid at: {invoiceData.settings.paymentDate}
</div>
)}
</div>
)}
</div>
)}
{/* Payment Status Stamp */}
{invoiceData.settings?.paymentStatus && (
<div style={{
marginTop: '20px',
textAlign: 'center'
}}>
<div className={"invoice-payment-status-stamp"} style={{
display: 'inline-block',
transform: 'rotate(-15deg)',
border: `3px solid ${
invoiceData.settings.paymentStatus === 'PAID' ? '#22c55e' :
invoiceData.settings.paymentStatus === 'PARTIALLY PAID' ? '#f59e0b' : '#ef4444'
}`,
borderRadius: '8px',
padding: '8px 16px',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
fontSize: '24px',
fontWeight: 'bold',
color: invoiceData.settings.paymentStatus === 'PAID' ? '#22c55e' :
invoiceData.settings.paymentStatus === 'PARTIALLY PAID' ? '#f59e0b' : '#ef4444',
textAlign: 'center',
letterSpacing: '2px'
}}>
{invoiceData.settings.paymentStatus}
</div>
{/* Payment Date for PAID status */}
{invoiceData.settings?.paymentStatus === 'PAID' && invoiceData.settings?.paymentDate && (
<div style={{
marginTop: '10px',
fontSize: '12px',
color: '#22c55e',
fontWeight: 'bold'
}}>
Paid at: {invoiceData.settings.paymentDate}
</div>
)}
</div>
)}
</div>
)}
{/* Totals */}
<div style={{ textAlign: 'right', minWidth: '300px', width: '100%', maxWidth: '400px' }}>
<div className="invoice-totals-row" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 16px',
borderBottom: '1px solid #e9ecef',
minHeight: '44px'
}}>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>Subtotal</span>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
{formatCurrency(invoiceData.subtotal, true)}
</span>
</div>
{/* Dynamic Fees */}
{invoiceData.fees && invoiceData.fees.map((fee) => (
<div key={fee.id} className="invoice-totals-row" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 16px',
borderBottom: '1px solid #e9ecef',
minHeight: '44px'
}}>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
{fee.label || 'Fee'} {fee.type === 'percentage' ? `(${fee.value}%)` : ''}
</span>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
+{formatCurrency(fee.type === 'percentage' ? (invoiceData.subtotal * fee.value) / 100 : fee.value, true)}
</span>
</div>
))}
{/* Dynamic Discounts */}
{invoiceData.discounts && invoiceData.discounts.map((discount) => (
<div key={discount.id} className="invoice-totals-row" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '12px 16px',
borderBottom: '1px solid #e9ecef',
minHeight: '44px'
}}>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
{discount.label || 'Discount'} {discount.type === 'percentage' ? `(${discount.value}%)` : ''}
</span>
<span style={{ fontSize: '14px', color: '#000000', lineHeight: '1.4' }}>
-{formatCurrency(discount.type === 'percentage' ? (invoiceData.subtotal * discount.value) / 100 : discount.value, true)}
</span>
</div>
))}
<div className="invoice-total-final" style={{
padding: '16px',
backgroundColor: `${accentColor}10`, // 10 = ~6% opacity
borderTop: `2px solid ${accentColor}`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
minHeight: '56px'
}}>
<span style={{ fontSize: '18px', fontWeight: 'bold', color: accentColor, lineHeight: '1.4' }}>Total</span>
<span style={{
fontSize: '18px',
fontWeight: 'bold',
color: accentColor,
lineHeight: '1.4'
}}>
{formatCurrency(invoiceData.total, true)}
</span>
</div>
</div>
</div>
{/* Notes Section */}
{invoiceData.notes && (
<div style={{ marginBottom: '30px' }}>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#000000',
marginBottom: '8px'
}}>
Notes
</h3>
<p style={{
fontSize: '14px',
color: '#000000',
margin: '0',
lineHeight: '1.5'
}}>
{invoiceData.notes}
</p>
</div>
)}
{/* Footer */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginTop: '40px' }}>
{/* Thank you message */}
<div>
<p style={{ fontSize: '16px', color: '#000000', margin: '0' }}>
{invoiceData.thankYouMessage || 'Thank you for your business!'}
</p>
</div>
{/* Signature Line */}
<div style={{ textAlign: 'center' }}>
{/* Digital Signature */}
{invoiceData.digitalSignature ? (
<div style={{ marginBottom: '8px' }}>
<img
src={invoiceData.digitalSignature}
alt="Digital Signature"
style={{
maxWidth: '200px',
maxHeight: '200px',
objectFit: 'contain'
}}
/>
</div>
) : (
// More space for physical signature when no digital signature
<div style={{
height: '60px',
marginBottom: '8px',
display: 'flex',
alignItems: 'flex-end'
}}></div>
)}
<div style={{
width: '200px',
borderBottom: `2px solid ${accentColor}`,
marginBottom: '8px'
}}></div>
<p style={{ fontSize: '12px', color: '#666666', margin: '0' }}>
{invoiceData.authorizedSignedText || 'Authorized Signed'}
</p>
</div>
</div>
</div>
);
};
export default MinimalTemplate;

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

165
src/config/tools.js Normal file
View File

@@ -0,0 +1,165 @@
import { Edit3, Table, LinkIcon, Hash, Wand2, GitCompare, Type, Home, FileText } from 'lucide-react';
// Master tools configuration - single source of truth
export const TOOL_CATEGORIES = {
navigation: {
name: 'Navigation',
color: 'from-slate-500 to-slate-600',
hoverColor: 'slate-600',
textColor: 'text-slate-600',
hoverTextColor: 'hover:text-slate-700 dark:hover:text-slate-400'
},
editor: {
name: 'Editor',
color: 'from-blue-500 to-cyan-500',
hoverColor: 'blue-600',
textColor: 'text-blue-600',
hoverTextColor: 'hover:text-blue-700 dark:hover:text-blue-400'
},
encoder: {
name: 'Encoder',
color: 'from-purple-500 to-pink-500',
hoverColor: 'purple-600',
textColor: 'text-purple-600',
hoverTextColor: 'hover:text-purple-700 dark:hover:text-purple-400'
},
formatter: {
name: 'Formatter',
color: 'from-green-500 to-emerald-500',
hoverColor: 'green-600',
textColor: 'text-green-600',
hoverTextColor: 'hover:text-green-700 dark:hover:text-green-400'
},
analyzer: {
name: 'Analyzer',
color: 'from-orange-500 to-red-500',
hoverColor: 'orange-600',
textColor: 'text-orange-600',
hoverTextColor: 'hover:text-orange-700 dark:hover:text-orange-400'
},
non_tools: {
name: 'Site Navigation',
color: 'from-indigo-500 to-purple-500',
hoverColor: 'indigo-600',
textColor: 'text-indigo-600',
hoverTextColor: 'hover:text-indigo-700 dark:hover:text-indigo-400'
}
};
export const TOOLS = [
{
path: '/object-editor',
name: 'Object Editor',
icon: Edit3,
description: 'Visual editor for JSON and PHP serialized objects with mindmap visualization',
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor'],
category: 'editor'
},
{
path: '/table-editor',
name: 'Table Editor',
icon: Table,
description: 'Import, edit, and export tabular data from URLs, files, or paste CSV/JSON',
tags: ['Table', 'CSV', 'JSON', 'Data', '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',
name: 'Invoice Editor',
icon: FileText,
description: 'Create, edit, and export professional invoices with PDF generation',
tags: ['Invoice', 'PDF', 'Business', 'Billing', 'Export'],
category: 'editor'
},
{
path: '/url',
name: 'URL Encoder/Decoder',
icon: LinkIcon,
description: 'Encode and decode URLs and query parameters',
tags: ['URL', 'Encode', 'Decode'],
category: 'encoder'
},
{
path: '/base64',
name: 'Base64 Encoder/Decoder',
icon: Hash,
description: 'Convert text to Base64 and back with support for files',
tags: ['Base64', 'Encode', 'Binary'],
category: 'encoder'
},
{
path: '/beautifier',
name: 'Code Beautifier/Minifier',
icon: Wand2,
description: 'Format and minify JSON, XML, SQL, CSS, and HTML code',
tags: ['Format', 'Minify', 'Beautify'],
category: 'formatter'
},
{
path: '/diff',
name: 'Text Diff Checker',
icon: GitCompare,
description: 'Compare two texts and highlight differences line by line',
tags: ['Diff', 'Compare', 'Text'],
category: 'analyzer'
},
{
path: '/text-length',
name: 'Text Length Checker',
icon: Type,
description: 'Analyze text length, word count, and other text statistics',
tags: ['Text', 'Length', 'Statistics'],
category: 'analyzer'
}
];
// Non-tool navigation items (homepage, what's new, etc.)
export const NON_TOOLS = [
{
path: '/',
name: 'Home',
icon: Home,
description: 'Back to homepage',
category: 'non_tools'
}
];
// Navigation tools (for sidebar) - combines non-tools and tools
export const NAVIGATION_TOOLS = [
...NON_TOOLS,
...TOOLS
];
// Site configuration
export const SITE_CONFIG = {
domain: 'https://dewe.dev',
title: 'Dewe.Dev',
subtitle: 'Professional Developer Utilities',
slogan: 'Code faster, debug smarter, ship better',
description: 'Professional-grade utilities for modern developers',
year: new Date().getFullYear(),
totalTools: TOOLS.length
};
// Helper functions
export const getCategoryConfig = (categoryKey) => TOOL_CATEGORIES[categoryKey] || TOOL_CATEGORIES.navigation;
export const getToolsByCategory = (categoryKey) => TOOLS.filter(tool => tool.category === categoryKey);
export const getCategoryStats = () => {
const stats = {};
Object.keys(TOOL_CATEGORIES).forEach(key => {
if (key !== 'navigation') {
stats[key] = getToolsByCategory(key).length;
}
});
return stats;
};

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;

60
src/hooks/useAnalytics.js Normal file
View File

@@ -0,0 +1,60 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { trackPageView, trackToolUsage, trackSearch, trackThemeChange } from '../utils/analytics';
// Custom hook for analytics tracking in React components
export const useAnalytics = () => {
const location = useLocation();
// Track page views on route changes
useEffect(() => {
const path = location.pathname;
let title = 'Dewe.Dev';
// Generate meaningful page titles
switch (path) {
case '/':
title = 'Dewe.Dev - Professional Developer Utilities';
break;
case '/object-editor':
title = 'Object Editor - Dewe.Dev';
break;
case '/table-editor':
title = 'Table Editor - Dewe.Dev';
break;
case '/url':
title = 'URL Encoder/Decoder - Dewe.Dev';
break;
case '/base64':
title = 'Base64 Encoder/Decoder - Dewe.Dev';
break;
case '/beautifier':
title = 'Code Beautifier/Minifier - Dewe.Dev';
break;
case '/diff':
title = 'Text Diff Checker - Dewe.Dev';
break;
case '/text-length':
title = 'Text Length Checker - Dewe.Dev';
break;
case '/privacy':
title = 'Privacy Policy - Dewe.Dev';
break;
case '/terms':
title = 'Terms of Service - Dewe.Dev';
break;
default:
title = `${path} - Dewe.Dev`;
}
// Track the page view
trackPageView(path, title);
}, [location]);
// Return tracking functions for components to use
return {
trackToolUsage,
trackSearch,
trackThemeChange,
};
};

View File

@@ -0,0 +1,181 @@
import { useCallback, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
const useNavigationGuard = () => {
const location = useLocation();
const navigate = useNavigate();
const [showModal, setShowModal] = useState(false);
const [pendingNavigation, setPendingNavigation] = useState(null);
// Check if we're on a page that might have user data
const isDataPage = useCallback(() => {
const dataPages = ['/invoice-editor', '/object-editor', '/table-editor'];
return dataPages.some(page => location.pathname.startsWith(page));
}, [location.pathname]);
// Check if there's modified data (not sample data) for the CURRENT editor only
const hasUnsavedData = useCallback(() => {
try {
// Only check data for the current editor
if (location.pathname.startsWith('/invoice-editor')) {
const invoiceData = localStorage.getItem('currentInvoice');
if (invoiceData) {
const parsed = JSON.parse(invoiceData);
// Check if has user data (same logic as InvoiceEditor hasUserData)
const hasUserData = parsed.invoiceNumber ||
parsed.company?.name ||
parsed.client?.name ||
(parsed.items && parsed.items.length > 0);
if (!hasUserData) return false;
// Check if it's not sample data (exact same logic as InvoiceEditor hasModifiedData)
const sampleInvoiceData = {
invoiceNumber: 'INV-2024-001',
date: '2024-01-15',
dueDate: '2024-02-15',
company: {
name: 'DevTools Inc.',
address: '123 Tech Street',
city: 'San Francisco, CA 94105',
phone: '+1 (555) 123-4567',
email: 'billing@devtools.com',
logo: null,
bankName: 'Chase Bank',
accountName: 'DevTools Inc.',
accountNumber: '1234567890'
},
client: {
name: 'Acme Corporation',
address: '456 Business Ave',
city: 'New York, NY 10001',
phone: '+1 (555) 987-6543',
email: 'accounts@acme.com'
},
items: [
{ id: 1, description: 'Web Development Services', quantity: 40, rate: 125, amount: 5000 },
{ id: 2, description: 'UI/UX Design', quantity: 20, rate: 100, amount: 2000 },
{ id: 3, description: 'Project Management', quantity: 10, rate: 150, amount: 1500 }
],
fees: [
{ id: 1, label: 'Processing Fee', type: 'fixed', value: 50, amount: 50 }
],
discounts: [
{ id: 1, label: 'Early Payment Discount', type: 'percentage', value: 5, amount: 425 }
],
subtotal: 8500,
discount: 0,
total: 8125,
notes: 'Payment due within 30 days.',
thankYouMessage: 'Thank you for your business!',
authorizedSignedText: 'Authorized Signed',
digitalSignature: null,
settings: {
colorScheme: '#3B82F6',
currency: { code: 'USD', symbol: '$' },
thousandSeparator: true
}
};
const isSampleData = JSON.stringify(parsed) === JSON.stringify(sampleInvoiceData);
return !isSampleData;
}
}
if (location.pathname.startsWith('/object-editor')) {
const objectData = localStorage.getItem('objectEditorData');
if (objectData) {
const parsed = JSON.parse(objectData);
// Check if has user data
const hasUserData = parsed && Object.keys(parsed).length > 0;
if (!hasUserData) return false;
// Check if it's not sample data (same logic as ObjectEditor)
const sampleObjectData = {
name: "John Doe",
age: 30,
email: "john@example.com",
address: {
street: "123 Main St",
city: "New York",
zipCode: "10001"
},
hobbies: ["reading", "coding", "traveling"]
};
const isSampleData = JSON.stringify(parsed) === JSON.stringify(sampleObjectData);
return !isSampleData;
}
}
if (location.pathname.startsWith('/table-editor')) {
const tableData = localStorage.getItem('tableEditorData');
if (tableData) {
const parsed = JSON.parse(tableData);
// Check if has user data
const hasUserData = parsed && parsed.length > 0;
if (!hasUserData) return false;
// Check if it's not sample data (same logic as TableEditor)
const sampleTableData = [
{ id: "row_0", col_0: 1, col_1: "John Doe", col_2: "john@example.com", col_3: 25, col_4: "New York" },
{ id: "row_1", col_0: 2, col_1: "Jane Smith", col_2: "jane@example.com", col_3: 30, col_4: "Los Angeles" },
{ id: "row_2", col_0: 3, col_1: "Bob Johnson", col_2: "bob@example.com", col_3: 35, col_4: "Chicago" },
{ id: "row_3", col_0: 4, col_1: "Alice Brown", col_2: "alice@example.com", col_3: 28, col_4: "Houston" },
{ id: "row_4", col_0: 5, col_1: "Charlie Wilson", col_2: "charlie@example.com", col_3: 32, col_4: "Phoenix" }
];
const isSampleData = JSON.stringify(parsed) === JSON.stringify(sampleTableData);
return !isSampleData;
}
}
return false;
} catch (error) {
return false;
}
}, [location.pathname]);
// Safe navigation function
const navigateWithGuard = useCallback((to, options = {}) => {
// If we're not on a data page or there's no unsaved data, navigate normally
if (!isDataPage() || !hasUnsavedData()) {
navigate(to, options);
return;
}
// Show modal and store pending navigation
setPendingNavigation({ to, options });
setShowModal(true);
}, [isDataPage, hasUnsavedData, navigate]);
// Handle modal confirmation
const handleConfirm = useCallback(() => {
if (pendingNavigation) {
navigate(pendingNavigation.to, pendingNavigation.options);
}
setShowModal(false);
setPendingNavigation(null);
}, [navigate, pendingNavigation]);
// Handle modal cancellation
const handleCancel = useCallback(() => {
setShowModal(false);
setPendingNavigation(null);
}, []);
return {
navigateWithGuard,
hasUnsavedData: hasUnsavedData(),
isDataPage: isDataPage(),
showModal,
pendingNavigation,
handleConfirm,
handleCancel
};
};
export default useNavigationGuard;

View File

@@ -7,6 +7,22 @@
@layer base {
html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow-x: hidden;
width: 100%;
max-width: 100vw;
}
body {
overflow-x: hidden;
width: 100%;
max-width: 100vw;
}
#root {
overflow-x: hidden;
width: 100%;
max-width: 100vw;
min-width: 0;
}
code, pre {
@@ -31,7 +47,24 @@
@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 {
@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;
}
.scrollbar-hide {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
}

View File

@@ -2,6 +2,10 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { initBrowserCompat } from './utils/browserCompat';
// Initialize browser compatibility fixes before React renders
initBrowserCompat();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(

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

@@ -1,155 +1,229 @@
import React, { useState } from 'react';
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Type, Edit3 } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import { Search, Code, Terminal, Zap, Shield, Cpu, Sparkles, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
import ToolCard from '../components/ToolCard';
import { TOOLS, SITE_CONFIG } from '../config/tools';
import { useAnalytics } from '../hooks/useAnalytics';
import SEO from '../components/SEO';
const Home = () => {
console.log('🏠 NEW Home component loaded - Object Editor should be visible!');
const [searchTerm, setSearchTerm] = useState('');
const [mounted, setMounted] = useState(false);
const { trackSearch } = useAnalytics();
const tools = [
{
icon: Edit3,
title: 'Object Editor',
description: 'Visual editor for JSON and PHP serialized objects with mindmap visualization',
path: '/object-editor',
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor']
},
{
icon: Link2,
title: 'URL Encoder/Decoder',
description: 'Encode and decode URLs and query parameters',
path: '/url',
tags: ['URL', 'Encode', 'Decode']
},
{
icon: Hash,
title: 'Base64 Encoder/Decoder',
description: 'Convert text to Base64 and back with support for files',
path: '/base64',
tags: ['Base64', 'Encode', 'Binary']
},
{
icon: RefreshCw,
title: 'CSV ↔ JSON Converter',
description: 'Convert between CSV and JSON formats with custom delimiters',
path: '/csv-json',
tags: ['CSV', 'JSON', 'Convert']
},
{
icon: FileText,
title: 'Code Beautifier/Minifier',
description: 'Format and minify JSON, XML, SQL, CSS, and HTML code',
path: '/beautifier',
tags: ['Format', 'Minify', 'Beautify']
},
{
icon: GitCompare,
title: 'Text Diff Checker',
description: 'Compare two texts and highlight differences line by line',
path: '/diff',
tags: ['Diff', 'Compare', 'Text']
},
{
icon: Type,
title: 'Text Length Checker',
description: 'Analyze text length, word count, and other text statistics',
path: '/text-length',
tags: ['Text', 'Length', 'Statistics']
useEffect(() => {
setMounted(true);
}, []);
// Handle search with analytics tracking
const handleSearchChange = (e) => {
const value = e.target.value;
setSearchTerm(value);
// Track search after user stops typing (debounced)
if (value.length > 2) {
trackSearch(value);
}
];
};
const filteredTools = tools.filter(tool =>
tool.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
const filteredTools = TOOLS.filter(tool =>
tool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
);
return (
<div className="max-w-6xl mx-auto">
{/* Hero Section */}
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
Developer Tools
</h1>
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8">
Essential utilities for web developers - fast, local, and easy to use
</p>
{/* Search */}
<div className="relative max-w-md mx-auto">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Search tools..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
<>
<SEO
title="Home"
description="Free online developer tools for JSON editing, table manipulation, invoice generation, Base64 encoding, URL encoding, code beautification, and more. Privacy-first, no data storage."
keywords="developer tools, json editor, table editor, invoice generator, base64 encoder, url encoder, code beautifier, diff tool, web developer utilities"
path="/"
/>
<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-slate-900">
<div className="container mx-auto px-4 py-12">
<div className="text-center mb-16">
{/* Terminal-style header */}
<div className="inline-flex items-center gap-2 px-4 py-2 bg-slate-800 dark:bg-slate-700 rounded-full text-green-400 font-mono text-sm mb-8 shadow-lg">
<Terminal className="h-4 w-4" />
<span>~/dewe.dev $</span>
<span className="animate-pulse">_</span>
</div>
<h1 className="text-5xl md:text-7xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent mb-6">
{SITE_CONFIG.title}
</h1>
<p className="text-xl md:text-2xl text-slate-600 dark:text-slate-300 mb-4 max-w-3xl mx-auto leading-relaxed">
{SITE_CONFIG.subtitle}
</p>
<p className="text-lg text-slate-500 dark:text-slate-400 mb-12 max-w-2xl mx-auto">
{SITE_CONFIG.slogan} {SITE_CONFIG.description}
</p>
{/* Enhanced Search */}
<div className="relative max-w-lg mx-auto mb-8">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl blur opacity-20"></div>
<div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
<input
type="text"
placeholder="Search tools..."
value={searchTerm}
onChange={handleSearchChange}
className="w-full pl-12 pr-6 py-4 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm border border-slate-200 dark:border-slate-600 rounded-2xl text-slate-900 dark:text-slate-100 placeholder-slate-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300 shadow-lg"
/>
</div>
</div>
{/* Stats */}
<div className="flex flex-col sm:flex-row justify-center items-center gap-4 sm:gap-8 text-sm text-slate-500 dark:text-slate-400 mb-8">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span>{SITE_CONFIG.totalTools} Tools Available</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span>100% Client-Side</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
<span>Zero Data Collection</span>
</div>
</div>
{/* What's New Button */}
<div className="flex justify-center">
<Link
to="/release-notes"
className="group relative inline-flex items-center gap-3 px-8 py-4 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 text-white font-semibold rounded-2xl shadow-lg hover:shadow-xl hover:shadow-purple-500/25 transition-all duration-300 transform hover:scale-105 overflow-hidden"
>
{/* Animated background effect */}
<div className="absolute inset-0 bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{/* Sparkle effect */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="absolute top-2 left-4 w-1 h-1 bg-white rounded-full animate-ping"></div>
<div className="absolute top-4 right-6 w-1 h-1 bg-white rounded-full animate-ping" style={{ animationDelay: '0.5s' }}></div>
<div className="absolute bottom-3 left-8 w-1 h-1 bg-white rounded-full animate-ping" style={{ animationDelay: '1s' }}></div>
</div>
<div className="relative flex items-center gap-3">
<div className="p-2 bg-white/20 rounded-lg group-hover:bg-white/30 transition-colors duration-300">
<Sparkles className="h-5 w-5 text-white group-hover:rotate-12 transition-transform duration-300" />
</div>
<div>
<div className="text-lg font-bold">What's New</div>
<div className="text-sm text-white/80 group-hover:text-white transition-colors duration-300">
Latest updates & features
</div>
</div>
<ArrowRight className="h-5 w-5 text-white/80 group-hover:text-white group-hover:translate-x-1 transition-all duration-300" />
</div>
</Link>
</div>
</div>
{/* Tools Grid */}
<div className={`transition-all duration-1000 delay-300 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-20">
{filteredTools.map((tool, index) => (
<div
key={index}
className={`transition-all duration-500 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}
style={{ transitionDelay: `${400 + index * 100}ms` }}
>
<ToolCard
icon={tool.icon}
title={tool.name}
description={tool.description}
path={tool.path}
tags={tool.tags}
category={tool.category}
/>
</div>
))}
</div>
</div>
{/* No Results */}
{filteredTools.length === 0 && (
<div className="text-center py-20">
<div className="text-6xl mb-4">🔍</div>
<p className="text-slate-500 dark:text-slate-400 text-xl mb-2">
No tools found matching "{searchTerm}"
</p>
<p className="text-slate-400 dark:text-slate-500">
Try searching for "editor", "encode", or "format"
</p>
</div>
)}
{/* Features Section */}
<div className={`py-20 transition-all duration-1000 delay-700 ${mounted ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold text-slate-800 dark:text-white mb-4">
Built for Developers
</h2>
<p className="text-xl text-slate-600 dark:text-slate-300 max-w-2xl mx-auto">
Every tool is crafted with developer experience and performance in mind
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-blue-300 dark:hover:border-blue-600 transition-all duration-300 hover:shadow-xl hover:shadow-blue-500/10">
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<Zap className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
Lightning Fast
</h3>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
Optimized algorithms and local processing ensure instant results
</p>
</div>
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 transition-all duration-300 hover:shadow-xl hover:shadow-purple-500/10">
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<Shield className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
Privacy First
</h3>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
Your data never leaves your browser. Zero tracking, zero storage
</p>
</div>
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-green-300 dark:hover:border-green-600 transition-all duration-300 hover:shadow-xl hover:shadow-green-500/10">
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<Cpu className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
No Limits
</h3>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
Handle massive files and complex data without restrictions
</p>
</div>
<div className="group text-center p-8 rounded-2xl bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700 hover:border-indigo-300 dark:hover:border-indigo-600 transition-all duration-300 hover:shadow-xl hover:shadow-indigo-500/10">
<div className="bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-2xl w-16 h-16 flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform duration-300">
<Code className="h-8 w-8 text-white" />
</div>
<h3 className="text-xl font-bold text-slate-800 dark:text-white mb-3">
Dev Focused
</h3>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
Syntax highlighting, shortcuts, and workflows developers love
</p>
</div>
</div>
</div>
</div>
</div>
{/* Tools Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredTools.map((tool, index) => (
<ToolCard
key={index}
icon={tool.icon}
title={tool.title}
description={tool.description}
path={tool.path}
tags={tool.tags}
/>
))}
</div>
{/* No Results */}
{filteredTools.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500 dark:text-gray-400 text-lg">
No tools found matching "{searchTerm}"
</p>
</div>
)}
{/* Features */}
<div className="mt-16 grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="text-center">
<div className="bg-primary-100 dark:bg-primary-900 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<RefreshCw className="h-8 w-8 text-primary-600" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Lightning Fast
</h3>
<p className="text-gray-600 dark:text-gray-300">
All processing happens locally in your browser for maximum speed and privacy
</p>
</div>
<div className="text-center">
<div className="bg-primary-100 dark:bg-primary-900 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<FileText className="h-8 w-8 text-primary-600" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Handle Large Files
</h3>
<p className="text-gray-600 dark:text-gray-300">
Process large text files and data with ease, no size limitations
</p>
</div>
<div className="text-center">
<div className="bg-primary-100 dark:bg-primary-900 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<Code className="h-8 w-8 text-primary-600" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
Developer Friendly
</h3>
<p className="text-gray-600 dark:text-gray-300">
Clean interface with syntax highlighting and easy copy-paste functionality
</p>
</div>
</div>
</div>
</>
);
};

2993
src/pages/InvoiceEditor.js Normal file

File diff suppressed because it is too large Load Diff

414
src/pages/InvoicePreview.js Normal file
View File

@@ -0,0 +1,414 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Download, FileText } from 'lucide-react';
import html2pdf from 'html2pdf.js';
import MinimalTemplate from '../components/invoice-templates/MinimalTemplate';
// Available templates
const templates = {
minimal: {
name: 'Minimal',
description: 'Simple, professional layout',
component: MinimalTemplate
}
};
const InvoicePreview = () => {
const navigate = useNavigate();
const [invoiceData, setInvoiceData] = useState(null);
const [pdfPageSize, setPdfPageSize] = useState('A4');
const [isGenerating, setIsGenerating] = useState(false);
const [selectedTemplate] = useState('minimal');
// Calculate totals (same logic as InvoiceEditor)
const calculateTotals = (items, discount = 0, fees = [], discounts = []) => {
const subtotal = items.reduce((sum, item) => sum + (item.amount || 0), 0);
// Calculate total fees
const totalFees = fees.reduce((sum, fee) => {
const feeAmount = fee.type === 'percentage'
? (subtotal * fee.value) / 100
: fee.value;
return sum + feeAmount;
}, 0);
// Calculate total discounts
const totalDiscounts = discounts.reduce((sum, discountItem) => {
const discountAmount = discountItem.type === 'percentage'
? (subtotal * discountItem.value) / 100
: discountItem.value;
return sum + discountAmount;
}, 0);
const total = subtotal + totalFees - discount - totalDiscounts;
return { subtotal, total };
};
// Load invoice data from localStorage
useEffect(() => {
try {
const savedInvoice = localStorage.getItem('currentInvoice');
const savedPageSize = localStorage.getItem('pdfPageSize');
if (savedInvoice) {
const parsedInvoice = JSON.parse(savedInvoice);
// Recalculate totals and installments to ensure accuracy
const { subtotal, total } = calculateTotals(
parsedInvoice.items || [],
parsedInvoice.discount || 0,
parsedInvoice.fees || [],
parsedInvoice.discounts || []
);
// Update totals
parsedInvoice.subtotal = subtotal;
parsedInvoice.total = total;
// Recalculate installments if they exist
if (parsedInvoice.paymentTerms?.installments?.length > 0) {
const updatedInstallments = parsedInvoice.paymentTerms.installments.map(installment => {
if (installment.type === 'percentage') {
const baseAmount = parsedInvoice.paymentTerms?.type === 'downpayment'
? total - (parsedInvoice.paymentTerms?.downPayment?.amount || 0)
: total;
const amount = (baseAmount * (installment.percentage || 0)) / 100;
return { ...installment, amount };
}
return installment;
});
parsedInvoice.paymentTerms = {
...parsedInvoice.paymentTerms,
installments: updatedInstallments
};
}
setInvoiceData(parsedInvoice);
} else {
// No invoice data found, redirect to editor
navigate('/invoice-editor');
}
if (savedPageSize) {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load invoice data:', error);
navigate('/invoice-editor');
}
}, [navigate]);
// Format number with thousand separator
const formatNumber = (num) => {
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
if (!invoiceData?.settings?.thousandSeparator) return num.toFixed(decimalDigits);
return num.toLocaleString('en-US', { minimumFractionDigits: decimalDigits, maximumFractionDigits: decimalDigits });
};
// Format currency
const formatCurrency = (amount, useThousandSeparator = false) => {
const symbol = invoiceData?.settings?.currency?.symbol || '$';
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(decimalDigits);
return `${symbol} ${formattedAmount}`;
};
// Utility function to temporarily apply print-optimized styles
const applyPrintStyles = () => {
const element = document.getElementById('invoice-content');
if (!element) return null;
// Store original styles
const originalStyles = new Map();
// Find all table elements and store their original styles
const tables = element.querySelectorAll('table');
tables.forEach((table, index) => {
originalStyles.set(`table-${index}`, table.style.cssText);
table.style.borderCollapse = 'collapse';
});
// Find all th and td elements and apply print styles
const cells = element.querySelectorAll('th, td');
cells.forEach((cell, index) => {
originalStyles.set(`cell-${index}`, cell.style.cssText);
cell.style.verticalAlign = 'middle';
cell.style.padding = '4px 12px 20px';
cell.style.lineHeight = '1.4';
});
// Find all totals rows and apply print styles
const totalsRows = element.querySelectorAll('.invoice-totals-row, .invoice-total-final');
totalsRows.forEach((row, index) => {
originalStyles.set(`totals-${index}`, row.style.cssText);
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.justifyContent = 'space-between';
row.style.padding = '4px 12px 20px';
row.style.minHeight = '44px';
});
// Find all stamp cards and apply print styles
const stampCards = element.querySelectorAll('.invoice-payment-status-stamp');
stampCards.forEach((stampCard, index) => {
originalStyles.set(`stamp-${index}`, stampCard.style.cssText);
stampCard.style.padding = '4px 12px 20px';
});
// Find all cards and apply print styles
const fromToCards = element.querySelectorAll('.invoice-from-to-card');
fromToCards.forEach((fromToCard, index) => {
originalStyles.set(`fromTo-${index}`, fromToCard.style.cssText);
fromToCard.style.padding = '4px 12px 20px';
});
return originalStyles;
};
// Utility function to restore original styles
const restoreOriginalStyles = (originalStyles) => {
if (!originalStyles) return;
const element = document.getElementById('invoice-content');
if (!element) return;
// Restore table styles
const tables = element.querySelectorAll('table');
tables.forEach((table, index) => {
const originalStyle = originalStyles.get(`table-${index}`);
if (originalStyle !== undefined) {
table.style.cssText = originalStyle;
}
});
// Restore cell styles
const cells = element.querySelectorAll('th, td');
cells.forEach((cell, index) => {
const originalStyle = originalStyles.get(`cell-${index}`);
if (originalStyle !== undefined) {
cell.style.cssText = originalStyle;
}
});
// Restore totals row styles
const totalsRows = element.querySelectorAll('.invoice-totals-row, .invoice-total-final');
totalsRows.forEach((row, index) => {
const originalStyle = originalStyles.get(`totals-${index}`);
if (originalStyle !== undefined) {
row.style.cssText = originalStyle;
}
});
// Restore stamp card styles
const stampCards = element.querySelectorAll('.invoice-payment-status-stamp');
stampCards.forEach((stampCard, index) => {
const originalStyle = originalStyles.get(`stamp-${index}`);
if (originalStyle !== undefined) {
stampCard.style.cssText = originalStyle;
}
});
// Restore fromTo card styles
const fromToCards = element.querySelectorAll('.invoice-from-to-card');
fromToCards.forEach((fromToCard, index) => {
const originalStyle = originalStyles.get(`fromTo-${index}`);
if (originalStyle !== undefined) {
fromToCard.style.cssText = originalStyle;
}
});
};
// Generate PDF from the visible invoice
const handleDownloadPDF = async () => {
if (!invoiceData) return;
setIsGenerating(true);
let originalStyles = null;
try {
const element = document.getElementById('invoice-content');
if (!element) {
throw new Error('Invoice content not found');
}
// Apply print-optimized styles temporarily
originalStyles = applyPrintStyles();
// Small delay to ensure styles are applied
await new Promise(resolve => setTimeout(resolve, 100));
const opt = {
margin: [0.2, 0.4, 0.8, 0.4], // top, left, bottom, right margins in inches - increased bottom margin to prevent elements dropping
filename: `invoice-${invoiceData.invoiceNumber || 'draft'}.pdf`,
image: { type: 'png', quality: 0.98 },
html2canvas: {
useCORS: true,
backgroundColor: '#ffffff',
letterRendering: true
},
jsPDF: {
unit: 'in',
format: pdfPageSize === 'F4' ? [8.27, 13] : pdfPageSize.toLowerCase(), // F4 dimensions in inches
orientation: 'portrait'
},
pagebreak: {
mode: ['avoid-all', 'css', 'legacy'],
before: '.page-break-before',
after: '.page-break-after',
avoid: '.page-break-avoid'
}
};
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
// Restore original styles after a short delay
setTimeout(() => {
restoreOriginalStyles(originalStyles);
}, 500);
setIsGenerating(false);
}
};
// Navigate back to editor
const handleEditInvoice = () => {
// Ensure current invoice data is saved before navigating
if (invoiceData) {
try {
localStorage.setItem('currentInvoice', JSON.stringify(invoiceData));
} catch (error) {
console.error('Failed to save invoice data before edit:', error);
}
}
// Add a parameter to indicate we're editing existing data
navigate('/invoice-editor?mode=edit');
};
if (!invoiceData) {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading invoice...</p>
</div>
</div>
);
}
// Get selected template component
const SelectedTemplateComponent = templates[selectedTemplate].component;
return (
<div className="min-h-screen bg-gray-100 dark:bg-slate-900">
{/* Mobile Notice */}
<div className="lg:hidden bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800 p-4">
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-amber-600 dark:text-amber-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="flex-1">
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">
Desktop Mode Recommended
</h3>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
For the best preview experience and accurate PDF generation, please use desktop mode or a larger screen. The invoice preview is optimized for desktop viewing.
</p>
</div>
</div>
</div>
{/* Invoice Preview */}
<div className="max-w-5xl mx-auto p-4 sm:p-6" style={{ maxWidth: 'min(60rem, calc(100vw - 2rem))' }}>
<div className="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-gray-200 dark:border-slate-700 overflow-hidden">
{(
<div className="px-4 sm:px-6 py-4 border-b border-gray-200 dark:border-slate-700">
<div className="flex items-center justify-between flex-col sm:flex-row gap-4">
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Invoice Preview</h2>
<div className="hidden sm:flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<span></span>
<span>{pdfPageSize} Format</span>
</div>
</div>
<div className="flex items-center gap-2">
{/* Back Button */}
<button
onClick={handleEditInvoice}
className="flex items-center gap-2 px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
>
<ArrowLeft className="h-4 w-4" />
<span className="hidden sm:inline">Back</span>
</button>
{/* Paper Size Selector */}
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600 dark:text-gray-300">Size:</label>
<select
value={pdfPageSize}
onChange={(e) => {
setPdfPageSize(e.target.value);
localStorage.setItem('pdfPageSize', e.target.value);
}}
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100"
>
<option value="A4">A4</option>
<option value="F4">F4</option>
</select>
</div>
{/* Download PDF Button */}
<button
onClick={handleDownloadPDF}
disabled={isGenerating}
className="flex items-center gap-2 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors disabled:opacity-50"
>
<Download className="h-4 w-4" />
<span className="hidden sm:inline">{isGenerating ? 'Generating...' : 'Download PDF'}</span>
</button>
</div>
</div>
</div>
)}
<div className="p-4 sm:p-6">
<div className="bg-white rounded-lg shadow-lg overflow-x-auto">
{/* PDF-Ready Invoice Content */}
<div
id="invoice-content"
className="bg-white"
style={{
maxWidth: pdfPageSize === 'A4' ? '720px' : '750px',
width: '100%',
minHeight: pdfPageSize === 'A4' ? '720px' : '750px',
margin: '0 auto',
position: 'relative',
}}
>
<SelectedTemplateComponent
invoiceData={invoiceData}
formatNumber={formatNumber}
formatCurrency={formatCurrency}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default InvoicePreview;

View File

@@ -0,0 +1,548 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Download, FileText, Plus } from 'lucide-react';
import html2pdf from 'html2pdf.js';
const InvoicePreviewMinimal = () => {
const navigate = useNavigate();
const [invoiceData, setInvoiceData] = useState(null);
const [pdfPageSize, setPdfPageSize] = useState('A4');
const [isGenerating, setIsGenerating] = useState(false);
// Load invoice data from localStorage
useEffect(() => {
try {
const savedInvoice = localStorage.getItem('currentInvoice');
const savedPageSize = localStorage.getItem('pdfPageSize');
if (savedInvoice) {
const parsedInvoice = JSON.parse(savedInvoice);
setInvoiceData(parsedInvoice);
// Set page title with invoice number
document.title = `Invoice Preview - ${parsedInvoice.invoiceNumber || 'Draft'} | DevTools`;
} else {
// No invoice data, redirect back to editor
navigate('/invoice-editor');
}
if (savedPageSize) {
setPdfPageSize(savedPageSize);
}
} catch (error) {
console.error('Failed to load invoice data:', error);
navigate('/invoice-editor');
}
}, [navigate]);
// Format number with thousand separator
const formatNumber = (num) => {
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
if (!invoiceData?.settings?.thousandSeparator) return num.toFixed(decimalDigits);
return num.toLocaleString('en-US', { minimumFractionDigits: decimalDigits, maximumFractionDigits: decimalDigits });
};
// Format currency
const formatCurrency = (amount, useThousandSeparator = false) => {
const symbol = invoiceData?.settings?.currency?.symbol || '$';
const decimalDigits = invoiceData?.settings?.decimalDigits ?? 2;
const formattedAmount = useThousandSeparator ? formatNumber(amount) : amount.toFixed(decimalDigits);
return `${symbol}${formattedAmount}`;
};
// Utility function to temporarily apply print-optimized styles
const applyPrintStyles = () => {
const element = document.getElementById('minimal-invoice-content');
if (!element) return null;
// Store original styles
const originalStyles = new Map();
// Find all table elements and store their original styles
const tables = element.querySelectorAll('table');
tables.forEach((table, index) => {
originalStyles.set(`table-${index}`, table.style.cssText);
table.style.borderCollapse = 'collapse';
});
// Find all th and td elements and apply print styles
const cells = element.querySelectorAll('th, td');
cells.forEach((cell, index) => {
originalStyles.set(`cell-${index}`, cell.style.cssText);
cell.style.verticalAlign = 'middle';
cell.style.padding = '12px 16px';
cell.style.lineHeight = '1.4';
});
// Find all totals rows and apply print styles
const totalsRows = element.querySelectorAll('.invoice-totals-row, .invoice-total-final');
totalsRows.forEach((row, index) => {
originalStyles.set(`totals-${index}`, row.style.cssText);
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.justifyContent = 'space-between';
row.style.padding = '12px 16px';
row.style.minHeight = '44px';
});
return originalStyles;
};
// Utility function to restore original styles
const restoreOriginalStyles = (originalStyles) => {
if (!originalStyles) return;
const element = document.getElementById('minimal-invoice-content');
if (!element) return;
// Restore table styles
const tables = element.querySelectorAll('table');
tables.forEach((table, index) => {
const originalStyle = originalStyles.get(`table-${index}`);
if (originalStyle !== undefined) {
table.style.cssText = originalStyle;
}
});
// Restore cell styles
const cells = element.querySelectorAll('th, td');
cells.forEach((cell, index) => {
const originalStyle = originalStyles.get(`cell-${index}`);
if (originalStyle !== undefined) {
cell.style.cssText = originalStyle;
}
});
// Restore totals row styles
const totalsRows = element.querySelectorAll('.invoice-totals-row, .invoice-total-final');
totalsRows.forEach((row, index) => {
const originalStyle = originalStyles.get(`totals-${index}`);
if (originalStyle !== undefined) {
row.style.cssText = originalStyle;
}
});
};
// Generate PDF from the visible invoice
const handleDownloadPDF = async () => {
if (!invoiceData) return;
setIsGenerating(true);
let originalStyles = null;
try {
const element = document.getElementById('minimal-invoice-content');
if (!element) {
throw new Error('Invoice content not found');
}
// Apply print-optimized styles temporarily
originalStyles = applyPrintStyles();
// Small delay to ensure styles are applied
await new Promise(resolve => setTimeout(resolve, 100));
const opt = {
margin: [20, 20, 40, 20], // Increased bottom margin to prevent elements dropping
filename: `Invoice-${invoiceData.invoiceNumber || new Date().toISOString().split('T')[0]}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
letterRendering: true,
allowTaint: true,
backgroundColor: '#ffffff',
width: pdfPageSize === 'A4' ? 794 : 816,
height: pdfPageSize === 'A4' ? 1123 : 1248
},
jsPDF: {
unit: 'px',
format: pdfPageSize === 'A4' ? [794, 1123] : [816, 1248],
orientation: 'portrait'
}
};
await html2pdf().set(opt).from(element).save();
} catch (error) {
console.error('PDF generation failed:', error);
alert('Failed to generate PDF. Please try again.');
} finally {
// Restore original styles after a short delay
setTimeout(() => {
restoreOriginalStyles(originalStyles);
}, 500);
setIsGenerating(false);
}
};
// Navigate back to editor
const handleEditInvoice = () => {
navigate('/invoice-editor');
};
// Create new invoice
const handleNewInvoice = () => {
localStorage.removeItem('currentInvoice');
navigate('/invoice-editor');
};
if (!invoiceData) {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading invoice...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-100">
{/* Top Action Bar */}
<div className="bg-white shadow-sm border-b sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 py-3">
<div className="flex items-center justify-between">
{/* Left Actions */}
<div className="flex items-center gap-4">
<button
onClick={handleEditInvoice}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
>
<ArrowLeft className="h-4 w-4" />
Edit Invoice
</button>
<div className="h-6 w-px bg-gray-300"></div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<FileText className="h-4 w-4" />
<span className="font-medium">Minimal Invoice Preview</span>
<span className="text-gray-400"></span>
<span>{pdfPageSize} Format</span>
</div>
</div>
{/* Right Actions */}
<div className="flex items-center gap-3">
<button
onClick={handleNewInvoice}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition-colors"
>
<Plus className="h-4 w-4" />
New Invoice
</button>
<button
onClick={handleDownloadPDF}
disabled={isGenerating}
className="flex items-center gap-2 px-6 py-2 bg-amber-500 hover:bg-amber-600 disabled:bg-amber-400 text-white rounded-lg transition-colors font-medium"
>
<Download className="h-4 w-4" />
{isGenerating ? 'Generating...' : 'Download PDF'}
</button>
</div>
</div>
</div>
</div>
{/* Invoice Preview */}
<div className="max-w-4xl mx-auto p-6">
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
{/* Minimal Invoice Content - Recreating the exact design from image */}
<div
id="minimal-invoice-content"
className="bg-white"
style={{
width: pdfPageSize === 'A4' ? '794px' : '816px',
minHeight: pdfPageSize === 'A4' ? '1123px' : '1248px',
margin: '0 auto',
padding: '60px',
fontSize: '14px',
lineHeight: '1.5',
fontFamily: 'system-ui, -apple-system, sans-serif',
color: '#000000'
}}
>
{/* Header Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '80px' }}>
{/* Company Logo & Name */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* Golden Star Logo */}
<div style={{
width: '40px',
height: '40px',
background: '#D4AF37',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
color: 'white',
fontWeight: 'bold'
}}>
</div>
<div>
<h1 style={{
fontSize: '24px',
fontWeight: 'bold',
color: '#000000',
margin: '0',
lineHeight: '1.2'
}}>
{invoiceData.company.name || 'Borcelle'}
</h1>
<p style={{
fontSize: '14px',
color: '#666666',
margin: '2px 0 0 0',
fontWeight: '400'
}}>
Meet All Your Needs
</p>
</div>
</div>
{/* Invoice Title */}
<h2 style={{
fontSize: '48px',
fontWeight: 'bold',
color: '#000000',
margin: '0',
letterSpacing: '2px'
}}>
INVOICE
</h2>
</div>
{/* Invoice Details Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '60px' }}>
{/* Invoice To */}
<div style={{ flex: 1 }}>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#000000',
marginBottom: '12px'
}}>
Invoice to:
</h3>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: '#000000', marginBottom: '8px' }}>
{invoiceData.client.name || 'Daniel Gallego'}
</div>
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div>{invoiceData.client.address || '123 Anywhere St.,'}</div>
<div>{invoiceData.client.city || 'Any City, ST 12345'}</div>
</div>
</div>
{/* Invoice Number & Date */}
<div style={{ textAlign: 'right', minWidth: '200px' }}>
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '16px', fontWeight: 'bold', color: '#000000' }}>Invoice# </span>
<span style={{ fontSize: '16px', color: '#000000', marginLeft: '20px' }}>
{invoiceData.invoiceNumber || '52131'}
</span>
</div>
<div>
<span style={{ fontSize: '16px', fontWeight: 'bold', color: '#000000' }}>Date </span>
<span style={{ fontSize: '16px', color: '#000000', marginLeft: '20px' }}>
{invoiceData.date || '01/02/2023'}
</span>
</div>
</div>
</div>
{/* Items Table */}
<div style={{ marginBottom: '40px' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
{/* Table Header */}
<thead>
<tr style={{ borderBottom: '2px solid #000000' }}>
<th style={{
padding: '12px 0',
textAlign: 'left',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Item</th>
<th style={{
padding: '12px 0',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Quantity</th>
<th style={{
padding: '12px 0',
textAlign: 'center',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Unit Price</th>
<th style={{
padding: '12px 0',
textAlign: 'right',
fontSize: '16px',
fontWeight: 'bold',
color: '#000000'
}}>Total</th>
</tr>
</thead>
{/* Table Body */}
<tbody>
{invoiceData.items.map((item, index) => (
<tr key={item.id} style={{
borderBottom: '1px solid #E5E5E5'
}}>
<td style={{
padding: '16px 0',
fontSize: '14px',
color: '#000000'
}}>
{item.description}
</td>
<td style={{
padding: '16px 0',
textAlign: 'center',
fontSize: '14px',
color: '#000000'
}}>
{formatNumber(item.quantity)}
</td>
<td style={{
padding: '16px 0',
textAlign: 'center',
fontSize: '14px',
color: '#000000'
}}>
{formatCurrency(item.rate, true)}
</td>
<td style={{
padding: '16px 0',
textAlign: 'right',
fontSize: '14px',
color: '#000000'
}}>
{formatCurrency(item.amount, true)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Payment Method & Totals Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '80px' }}>
{/* Payment Method */}
<div style={{ flex: 1, maxWidth: '300px' }}>
<h3 style={{
fontSize: '16px',
fontWeight: 'bold',
color: '#000000',
marginBottom: '16px'
}}>
PAYMENT METHOD
</h3>
<div style={{ fontSize: '14px', color: '#000000', lineHeight: '1.6' }}>
<div style={{ marginBottom: '4px' }}>Rimberio Bank</div>
<div style={{ marginBottom: '4px' }}>Account Name: Alfredo Torres</div>
<div style={{ marginBottom: '4px' }}>Account No.: 0123 4567 8901</div>
<div>Pay by: 23 June 2023</div>
</div>
</div>
{/* Totals */}
<div style={{ textAlign: 'right', minWidth: '200px' }}>
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>Subtotal</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
{formatCurrency(invoiceData.subtotal, true)}
</span>
</div>
{invoiceData.taxRate > 0 && (
<div style={{ marginBottom: '12px' }}>
<span style={{ fontSize: '14px', color: '#000000' }}>
Tax ({formatNumber(invoiceData.taxRate)}%)
</span>
<span style={{ fontSize: '14px', color: '#000000', marginLeft: '40px' }}>
{formatCurrency(invoiceData.taxAmount, true)}
</span>
</div>
)}
<div style={{
borderTop: '1px solid #000000',
paddingTop: '12px',
marginTop: '16px'
}}>
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#000000' }}>Total</span>
<span style={{
fontSize: '18px',
fontWeight: 'bold',
color: '#000000',
marginLeft: '40px'
}}>
{formatCurrency(invoiceData.total, true)}
</span>
</div>
</div>
</div>
{/* Footer Section */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginTop: '120px' }}>
{/* Thank You Message */}
<div>
<p style={{ fontSize: '16px', color: '#000000', margin: '0' }}>
Thank you for your business!
</p>
</div>
{/* Signature Line */}
<div style={{ textAlign: 'center' }}>
<div style={{
width: '200px',
borderBottom: '2px solid #D4AF37',
marginBottom: '8px'
}}></div>
<p style={{ fontSize: '12px', color: '#666666', margin: '0' }}>
Authorized Signed
</p>
</div>
</div>
{/* Bottom Golden Bar */}
<div style={{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '60px',
background: '#D4AF37',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '40px',
color: 'white',
fontSize: '14px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>📞</span>
<span>123-456-7890</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>📍</span>
<span>123 Anywhere St., Any City</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default InvoicePreviewMinimal;

View File

@@ -1,247 +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';
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' ? (
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Paste your JSON here..."
className="tool-input h-96"
/>
) : (
<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">
<textarea
value={output}
readOnly
placeholder="Formatted JSON 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> 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;

File diff suppressed because it is too large Load Diff

265
src/pages/PrivacyPolicy.js Normal file
View File

@@ -0,0 +1,265 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowLeft, Shield, Lock, Eye, Server, Cookie, BarChart3, Globe } from 'lucide-react';
import { SITE_CONFIG } from '../config/tools';
const PrivacyPolicy = () => {
return (
<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">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Header */}
<div className="mb-8">
<Link
to="/"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors mb-6"
>
<ArrowLeft className="h-4 w-4" />
Back to Home
</Link>
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-gradient-to-br from-green-500 to-emerald-500 rounded-xl shadow-lg">
<Lock className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-slate-800 dark:text-white">
Privacy Policy
</h1>
<p className="text-slate-600 dark:text-slate-300">
Last updated: {new Date().toLocaleDateString()}
</p>
</div>
</div>
</div>
{/* Content */}
<div className="bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm rounded-2xl border border-slate-200 dark:border-slate-700 p-8 shadow-xl">
<div className="prose prose-slate dark:prose-invert max-w-none">
{/* Privacy-First Commitment */}
<section className="mb-8">
<div className="bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 rounded-xl p-6 mb-6">
<h2 className="text-2xl font-bold text-slate-800 dark:text-white mb-4 flex items-center gap-3">
<Shield className="h-6 w-6 text-green-600" />
Our Privacy-First Commitment
</h2>
<p className="text-slate-700 dark:text-slate-200 text-lg leading-relaxed mb-4">
At {SITE_CONFIG.title}, "Privacy-First" isn't just a marketing term—it's our core architectural principle. Your data privacy is protected by design, not by policy alone.
</p>
<div className="grid md:grid-cols-2 gap-4">
<div className="flex items-start gap-3">
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
<Server className="h-5 w-5 text-green-600" />
</div>
<div>
<h3 className="font-semibold text-slate-800 dark:text-white">100% Client-Side</h3>
<p className="text-sm text-slate-600 dark:text-slate-300">All processing happens in your browser</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<Lock className="h-5 w-5 text-blue-600" />
</div>
<div>
<h3 className="font-semibold text-slate-800 dark:text-white">Zero Data Upload</h3>
<p className="text-sm text-slate-600 dark:text-slate-300">Your sensitive data never leaves your device</p>
</div>
</div>
</div>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Eye className="h-5 w-5 text-blue-600" />
1. Information We Collect
</h2>
<div className="space-y-6">
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-4">
<h3 className="font-semibold text-red-800 dark:text-red-200 mb-2">
What We DON'T Collect:
</h3>
<ul className="list-disc list-inside text-red-700 dark:text-red-300 space-y-1 text-sm">
<li>Your input data (JSON, CSV, URLs, text, etc.)</li>
<li>Files you upload or paste into our tools</li>
<li>Personal information (name, email, address)</li>
<li>Login credentials or user accounts</li>
<li>IP addresses or device fingerprints</li>
<li>Browsing history or cross-site tracking</li>
</ul>
</div>
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
<h3 className="font-semibold text-green-800 dark:text-green-200 mb-2">
✅ What We DO Collect (via Google Analytics):
</h3>
<ul className="list-disc list-inside text-green-700 dark:text-green-300 space-y-1 text-sm">
<li>Anonymous page views and session duration</li>
<li>Which tools are most popular (aggregated data only)</li>
<li>General geographic region (country/state level)</li>
<li>Browser type and device type (for compatibility)</li>
<li>Referral sources (how you found our site)</li>
</ul>
</div>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<BarChart3 className="h-5 w-5 text-purple-600" />
2. Google Analytics Usage
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed mb-4">
We use Google Analytics to understand how our tools are used and to improve the service. This helps us answer questions like:
</p>
<ul className="list-disc list-inside text-slate-600 dark:text-slate-300 space-y-2 ml-4 mb-4">
<li>Which tools are most helpful to developers?</li>
<li>Are there performance issues on certain devices?</li>
<li>How can we improve the user experience?</li>
</ul>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<p className="text-blue-800 dark:text-blue-200 text-sm">
<strong>Important:</strong> Google Analytics only sees that someone visited "dewe.dev/beautifier" - it never sees the actual JSON code you're beautifying or any data you process with our tools.
</p>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Server className="h-5 w-5 text-indigo-600" />
3. How Our Tools Work
</h2>
<div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-6">
<h3 className="font-semibold text-slate-800 dark:text-white mb-3">Technical Architecture:</h3>
<div className="space-y-3 text-sm">
<div className="flex items-start gap-3">
<span className="bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 px-2 py-1 rounded text-xs font-medium">CLIENT</span>
<p className="text-slate-600 dark:text-slate-300">Your browser downloads our JavaScript code</p>
</div>
<div className="flex items-start gap-3">
<span className="bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 px-2 py-1 rounded text-xs font-medium">LOCAL</span>
<p className="text-slate-600 dark:text-slate-300">All processing happens locally in your browser's memory</p>
</div>
<div className="flex items-start gap-3">
<span className="bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-2 py-1 rounded text-xs font-medium">SECURE</span>
<p className="text-slate-600 dark:text-slate-300">No data transmission to our servers for processing</p>
</div>
</div>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Cookie className="h-5 w-5 text-orange-600" />
4. Cookies and Local Storage
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed mb-4">
We use minimal cookies and local storage for:
</p>
<ul className="list-disc list-inside text-slate-600 dark:text-slate-300 space-y-2 ml-4">
<li><strong>Google Analytics:</strong> Anonymous tracking cookies (you can opt-out)</li>
<li><strong>Theme Preference:</strong> Remembering if you prefer dark/light mode</li>
<li><strong>No Personal Data:</strong> We never store your processed data locally</li>
</ul>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Globe className="h-5 w-5 text-teal-600" />
5. Future Advertising (Google AdSense)
</h2>
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4 mb-4">
<h3 className="font-semibold text-amber-800 dark:text-amber-200 mb-2">
🔮 Planned Implementation:
</h3>
<p className="text-amber-700 dark:text-amber-300 text-sm leading-relaxed mb-3">
To keep our tools free, we plan to display Google AdSense advertisements. When implemented:
</p>
<ul className="list-disc list-inside text-amber-700 dark:text-amber-300 space-y-1 text-sm">
<li>Ads will be clearly marked and non-intrusive</li>
<li>No impact on tool functionality or performance</li>
<li>Google may use cookies for ad personalization</li>
<li>You can opt-out of personalized ads via Google settings</li>
<li><strong>We will NEVER share your tool usage data with advertisers</strong></li>
</ul>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
6. Your Rights and Controls
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<h3 className="font-semibold text-blue-800 dark:text-blue-200 mb-2">Analytics Opt-Out:</h3>
<p className="text-blue-700 dark:text-blue-300 text-sm">
Install browser extensions like uBlock Origin or use Google's opt-out tools to disable analytics tracking.
</p>
</div>
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
<h3 className="font-semibold text-green-800 dark:text-green-200 mb-2">Data Control:</h3>
<p className="text-green-700 dark:text-green-300 text-sm">
Since we don't collect your data, there's nothing to delete or export. Your data stays with you.
</p>
</div>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
7. Third-Party Services
</h2>
<div className="space-y-4">
<div className="border-l-4 border-blue-500 pl-4">
<h3 className="font-semibold text-slate-800 dark:text-white">Google Analytics</h3>
<p className="text-slate-600 dark:text-slate-300 text-sm">
Privacy Policy: <a href="https://policies.google.com/privacy" className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">https://policies.google.com/privacy</a>
</p>
</div>
<div className="border-l-4 border-green-500 pl-4">
<h3 className="font-semibold text-slate-800 dark:text-white">Google AdSense (Future)</h3>
<p className="text-slate-600 dark:text-slate-300 text-sm">
Privacy Policy: <a href="https://policies.google.com/privacy" className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">https://policies.google.com/privacy</a>
</p>
</div>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
8. Changes to This Policy
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
We may update this privacy policy from time to time. We will notify users of any material changes by updating the "Last updated" date at the top of this policy. Your continued use of the service after any changes constitutes acceptance of the new policy.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
9. Contact Us
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
If you have any questions about this Privacy Policy or our privacy practices, please contact us at{' '}
<a href="mailto:dewe.developer@gmail.com" className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
dewe.developer@gmail.com
</a>
{' '}or visit {SITE_CONFIG.domain}.
</p>
</section>
</div>
</div>
{/* Footer */}
<div className="mt-8 text-center">
<p className="text-sm text-slate-500 dark:text-slate-400">
© {SITE_CONFIG.year} {SITE_CONFIG.title} Your privacy is our priority
</p>
</div>
</div>
</div>
);
};
export default PrivacyPolicy;

358
src/pages/ReleaseNotes.js Normal file
View File

@@ -0,0 +1,358 @@
import React, { useState, useEffect } from 'react';
import { Calendar, Sparkles, Bug, Zap, Shield, ChevronDown, ChevronUp } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
const ReleaseNotes = () => {
const [releases, setReleases] = useState([]);
const [loading, setLoading] = useState(true);
const [expandedReleases, setExpandedReleases] = useState(new Set());
// Parse commit messages into user-friendly release notes (keeping local version for now)
// eslint-disable-next-line no-unused-vars
const parseCommitMessage = (message) => {
// Skip non-user-informative commits
const skipPatterns = [
/^fix eslint/i,
/^remove.*eslint/i,
/^update.*package/i,
/^add debug/i,
/^fix.*dependency/i,
/deployment/i,
/^fix.*mismatch/i
];
if (skipPatterns.some(pattern => pattern.test(message))) {
return null;
}
// Transform commit messages to user-friendly descriptions
const transformations = [
{
pattern: /feat.*invoice.*editor.*improvements/i,
type: 'feature',
title: 'Invoice Editor Major Update',
description: 'Complete overhaul of Invoice Editor with currency system, PDF generation fixes, improved UI/UX, removed print functionality (use PDF download instead), streamlined preview toolbar, and comprehensive bug fixes'
},
{
pattern: /feat.*enhanced.*what.*new.*feature.*non_tools.*category.*global.*footer/i,
type: 'feature',
title: 'What\'s New Feature & Navigation Improvements',
description: 'Added attractive "What\'s New" button to homepage, created NON_TOOLS category for better navigation organization, separated navigation items in sidebar and mobile menu, and implemented unified global footer across all pages'
},
{
pattern: /improve.*objecteditor.*postmantable.*ui\/ux/i,
type: 'enhancement',
title: 'Enhanced Object Editor & Table View',
description: 'Improved user interface and experience with better JSON parsing, HTML rendering, and copy functionality'
},
{
pattern: /feat.*analytics.*mobile.*ui/i,
type: 'feature',
title: 'Mobile UI Improvements',
description: 'Optimized interface for mobile devices with better analytics integration'
},
{
pattern: /feat.*seo.*gdpr/i,
type: 'feature',
title: 'SEO & Privacy Compliance',
description: 'Comprehensive SEO optimization and GDPR compliance features for better discoverability and privacy protection'
},
{
pattern: /improve.*objecteditor.*tableeditor/i,
type: 'enhancement',
title: 'Enhanced Data Editors',
description: 'Major improvements to Object Editor and new Table Editor with advanced data manipulation features'
},
{
pattern: /enhanced.*object.*editor.*fetch.*mobile/i,
type: 'feature',
title: 'Object Editor with Data Fetching',
description: 'Added ability to fetch data from URLs directly in Object Editor with mobile-optimized interface'
},
{
pattern: /complete.*postman.*table.*view/i,
type: 'feature',
title: 'Postman-Style Table View',
description: 'New professional table visualization with consistent design and advanced data exploration features'
},
{
pattern: /enhanced.*mindmap.*visualization/i,
type: 'feature',
title: 'Professional Mindmap Visualization',
description: 'Beautiful mindmap interface for visualizing complex data structures with interactive navigation'
},
{
pattern: /add.*text.*length.*checker/i,
type: 'feature',
title: 'Text Analysis Tool',
description: 'New comprehensive text analysis tool with length checking and detailed text statistics'
},
{
pattern: /fix.*php.*serialization.*long.*text/i,
type: 'fix',
title: 'PHP Serialization Improvements',
description: 'Fixed PHP serialization handling and added support for long text fields in Visual Editor'
},
{
pattern: /enhanced.*developer.*tools.*ux/i,
type: 'enhancement',
title: 'Developer Tools UX Enhancement',
description: 'Improved overall user experience with visual enhancements and better tool organization'
}
];
for (const transform of transformations) {
if (transform.pattern.test(message)) {
return {
type: transform.type,
title: transform.title,
description: transform.description
};
}
}
// Fallback for unmatched patterns
if (message.includes('🐛') || message.toLowerCase().includes('fix')) {
return {
type: 'fix',
title: 'Bug Fixes',
description: message.replace(/🐛|fix/gi, '').trim()
};
}
if (message.includes('✨') || message.toLowerCase().includes('feat')) {
return {
type: 'feature',
title: 'New Feature',
description: message.replace(/✨|feat:/gi, '').trim()
};
}
return null;
};
// Get type icon and color
const getTypeConfig = (type) => {
const configs = {
feature: {
icon: <Sparkles className="h-4 w-4" />,
color: 'text-blue-600 dark:text-blue-400',
bgColor: 'bg-blue-100 dark:bg-blue-900/20',
label: 'New Feature'
},
enhancement: {
icon: <Zap className="h-4 w-4" />,
color: 'text-purple-600 dark:text-purple-400',
bgColor: 'bg-purple-100 dark:bg-purple-900/20',
label: 'Enhancement'
},
fix: {
icon: <Bug className="h-4 w-4" />,
color: 'text-green-600 dark:text-green-400',
bgColor: 'bg-green-100 dark:bg-green-900/20',
label: 'Bug Fix'
},
security: {
icon: <Shield className="h-4 w-4" />,
color: 'text-red-600 dark:text-red-400',
bgColor: 'bg-red-100 dark:bg-red-900/20',
label: 'Security'
}
};
return configs[type] || configs.enhancement;
};
// Group releases by date
const groupReleasesByDate = (releases) => {
const grouped = {};
releases.forEach(release => {
const date = new Date(release.date).toDateString();
if (!grouped[date]) {
grouped[date] = [];
}
grouped[date].push(release);
});
return grouped;
};
const toggleRelease = (date) => {
const newExpanded = new Set(expandedReleases);
if (newExpanded.has(date)) {
newExpanded.delete(date);
} else {
newExpanded.add(date);
}
setExpandedReleases(newExpanded);
};
useEffect(() => {
// Load release data from commits.json
const fetchReleases = async () => {
setLoading(true);
try {
const response = await fetch('/data/commits.json');
const data = await response.json();
// Transform changelog data to release format
const releases = [];
data.changelog.forEach(dateEntry => {
dateEntry.changes.forEach(change => {
releases.push({
id: `${dateEntry.date}-${change.type}-${change.title.replace(/\s+/g, '-')}`,
date: change.datetime || dateEntry.date, // Use datetime if available, fallback to date
type: change.type,
title: change.title,
description: change.description
});
});
});
setReleases(releases);
} catch (error) {
console.error('Failed to load commits.json:', error);
setReleases([]);
} finally {
setLoading(false);
}
};
fetchReleases();
}, []);
const groupedReleases = groupReleasesByDate(releases);
return (
<ToolLayout
title="What's New"
description="Stay updated with the latest features, improvements, and bug fixes"
>
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-12">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl mb-6">
<Sparkles className="h-8 w-8 text-white" />
</div>
<h1 className="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-4">
What's New
</h1>
<p className="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
Discover the latest features, improvements, and bug fixes that make your development workflow even better.
</p>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
) : (
<div className="relative">
{/* Timeline line */}
<div className="absolute left-8 top-0 bottom-0 w-0.5 bg-gradient-to-b from-blue-500 via-purple-500 to-transparent"></div>
<div className="space-y-6">
{Object.entries(groupedReleases)
.sort(([a], [b]) => new Date(b) - new Date(a))
.map(([date, dayReleases], index) => {
const isExpanded = expandedReleases.has(date);
const releaseDate = new Date(date);
const isRecent = (new Date() - releaseDate) < 7 * 24 * 60 * 60 * 1000;
return (
<div key={date} className="relative">
{/* Timeline dot */}
<div className="absolute left-6 top-6 w-4 h-4 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full border-4 border-white dark:border-gray-900 shadow-lg z-10"></div>
<div className="ml-16 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Date Header */}
<button
onClick={() => toggleRelease(date)}
className="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div className="flex items-center space-x-3">
<Calendar className="h-5 w-5 text-gray-500 dark:text-gray-400" />
<div className="text-left">
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
{releaseDate.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{dayReleases.length} update{dayReleases.length !== 1 ? 's' : ''}
{isRecent && <span className="ml-2 text-blue-600 dark:text-blue-400">• Recent</span>}
</p>
</div>
</div>
{isExpanded ? (
<ChevronUp className="h-5 w-5 text-gray-400" />
) : (
<ChevronDown className="h-5 w-5 text-gray-400" />
)}
</button>
{/* Release Items */}
{isExpanded && (
<div className="border-t border-gray-200 dark:border-gray-700">
{dayReleases.map((release, index) => {
const typeConfig = getTypeConfig(release.type);
return (
<div key={release.id} className={`p-6 ${index !== dayReleases.length - 1 ? 'border-b border-gray-100 dark:border-gray-700' : ''}`}>
<div className="flex flex-col md:flex-row items-start md:space-x-4">
<div className={`flex-shrink-0 p-2 rounded-lg ${typeConfig.bgColor} flex items-center space-x-2 mb-2`}>
<div className={typeConfig.color}>
{typeConfig.icon}
</div>
<span className={`block md:hidden px-2 py-1 text-xs font-medium rounded-full ${typeConfig.bgColor} ${typeConfig.color}`}>
{typeConfig.label}
</span>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-2">
<h4 className="font-semibold text-gray-900 dark:text-gray-100">
{release.title}
</h4>
<span className={`hidden md:block px-2 py-1 text-xs font-medium rounded-full ${typeConfig.bgColor} ${typeConfig.color}`}>
{typeConfig.label}
</span>
</div>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
{release.description}
</p>
<div className="mt-3 flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
<span>
{new Date(release.date).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
</div>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{/* Footer */}
<div className="text-center mt-12 py-8 border-t border-gray-200 dark:border-gray-700">
<p className="text-gray-500 dark:text-gray-400">
Stay tuned for more exciting updates and improvements!
</p>
</div>
</div>
</ToolLayout>
);
};
export default ReleaseNotes;

View File

@@ -1,553 +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);
console.log(`Parsing string with declared length: ${byteLength}, starting at position: ${index}`);
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;
console.log(`String parsing: declared ${byteLength} bytes, actual ${actualByteLength} bytes, content length ${stringVal.length} chars`);
console.log(`Extracted string: "${stringVal.substring(0, 50)}${stringVal.length > 50 ? '...' : ''}"`);
// Move index to after the closing '";'
index = endQuotePos + 2;
console.log(`After string parsing, index is at: ${index}, next chars: "${str.substring(index, index + 5)}"`);
// 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;

3899
src/pages/TableEditor.js Normal file

File diff suppressed because it is too large Load Diff

163
src/pages/TermsOfService.js Normal file
View File

@@ -0,0 +1,163 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowLeft, Shield, Code, Globe } from 'lucide-react';
import { SITE_CONFIG } from '../config/tools';
const TermsOfService = () => {
return (
<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">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Header */}
<div className="mb-8">
<Link
to="/"
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors mb-6"
>
<ArrowLeft className="h-4 w-4" />
Back to Home
</Link>
<div className="flex items-center gap-3 mb-4">
<div className="p-3 bg-gradient-to-br from-blue-500 to-purple-500 rounded-xl shadow-lg">
<Shield className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-slate-800 dark:text-white">
Terms of Service
</h1>
<p className="text-slate-600 dark:text-slate-300">
Last updated: {new Date().toLocaleDateString()}
</p>
</div>
</div>
</div>
{/* Content */}
<div className="bg-white/70 dark:bg-slate-800/70 backdrop-blur-sm rounded-2xl border border-slate-200 dark:border-slate-700 p-8 shadow-xl">
<div className="prose prose-slate dark:prose-invert max-w-none">
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Code className="h-5 w-5 text-blue-600" />
1. Acceptance of Terms
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
By accessing and using {SITE_CONFIG.title} ("{SITE_CONFIG.domain}"), you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to abide by the above, please do not use this service.
</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4 flex items-center gap-2">
<Globe className="h-5 w-5 text-green-600" />
2. Service Description
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed mb-4">
{SITE_CONFIG.title} provides a collection of developer tools including but not limited to:
</p>
<ul className="list-disc list-inside text-slate-600 dark:text-slate-300 space-y-2 ml-4">
<li>Object and Table Editors for JSON, CSV, and other data formats</li>
<li>URL and Base64 Encoders/Decoders</li>
<li>Code Beautifiers and Minifiers</li>
<li>Text Analysis and Comparison Tools</li>
<li>Other web-based developer utilities</li>
</ul>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed mt-4">
All tools run entirely in your browser - no data is sent to our servers for processing.
</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
3. Privacy-First Approach
</h2>
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mb-4">
<p className="text-blue-800 dark:text-blue-200 font-medium mb-2">
🔒 What "Privacy-First" means at {SITE_CONFIG.title}:
</p>
<ul className="list-disc list-inside text-blue-700 dark:text-blue-300 space-y-1 text-sm">
<li><strong>Client-Side Processing:</strong> All tools process your data locally in your browser</li>
<li><strong>No Data Upload:</strong> Your sensitive data never leaves your device</li>
<li><strong>No Storage:</strong> We don't store, cache, or log your input data</li>
<li><strong>Minimal Analytics:</strong> We only collect anonymous usage statistics via Google Analytics</li>
<li><strong>No Tracking:</strong> No user accounts, no personal data collection</li>
</ul>
</div>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
We use Google Analytics to understand how our tools are used (page views, popular tools, etc.) but we never track or store the actual data you process with our tools.
</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
4. Use License
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
Permission is granted to temporarily use {SITE_CONFIG.title} for personal and commercial purposes. This is the grant of a license, not a transfer of title, and under this license you may not:
</p>
<ul className="list-disc list-inside text-slate-600 dark:text-slate-300 space-y-2 ml-4 mt-4">
<li>Use the service for any illegal or unauthorized purpose</li>
<li>Attempt to reverse engineer or extract source code</li>
<li>Use automated tools to overload our servers</li>
<li>Redistribute or resell access to the service</li>
</ul>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
5. Disclaimer
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
The materials on {SITE_CONFIG.title} are provided on an 'as is' basis. {SITE_CONFIG.title} makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties including without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.
</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
6. Limitations
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
In no event shall {SITE_CONFIG.title} or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on {SITE_CONFIG.title}, even if {SITE_CONFIG.title} or an authorized representative has been notified orally or in writing of the possibility of such damage.
</p>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
7. Future Monetization
</h2>
<div className="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4">
<p className="text-amber-800 dark:text-amber-200 leading-relaxed">
<strong>Transparency Notice:</strong> We plan to implement Google AdSense advertisements in the future to support the free operation of this service. When implemented, ads will be clearly marked and will not interfere with tool functionality. Our privacy-first approach will remain unchanged - we will never sell or share your usage data with advertisers.
</p>
</div>
</section>
<section className="mb-8">
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
8. Revisions
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
{SITE_CONFIG.title} may revise these terms of service at any time without notice. By using this service, you are agreeing to be bound by the then current version of these terms of service.
</p>
</section>
<section>
<h2 className="text-xl font-semibold text-slate-800 dark:text-white mb-4">
9. Contact Information
</h2>
<p className="text-slate-600 dark:text-slate-300 leading-relaxed">
If you have any questions about these Terms of Service, please contact us at{' '}
<a href="mailto:dewe.developer@gmail.com" className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
dewe.developer@gmail.com
</a>
{' '}or through our website at {SITE_CONFIG.domain}.
</p>
</section>
</div>
</div>
</div>
</div>
);
};
export default TermsOfService;

View File

@@ -1,10 +1,16 @@
import React, { useState, useEffect } from 'react';
import { Type, Copy, RotateCcw } from 'lucide-react';
import { Type, Copy, RotateCcw, Globe, Download, AlertCircle, Clock, X } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
import { extractContentFromUrl, CONTENT_TYPE_INFO } from '../utils/contentExtractor';
const TextLengthTool = () => {
const [text, setText] = useState('');
const [url, setUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [urlResult, setUrlResult] = useState(null);
const [error, setError] = useState('');
const [useArticleOnly, setUseArticleOnly] = useState(true);
const [stats, setStats] = useState({
characters: 0,
charactersNoSpaces: 0,
@@ -65,8 +71,52 @@ const TextLengthTool = () => {
calculateStats();
}, [text]);
// Handle URL fetching
const fetchUrlContent = async () => {
if (!url.trim()) {
setError('Please enter a valid URL');
return;
}
setIsLoading(true);
setError('');
setUrlResult(null);
try {
const result = await extractContentFromUrl(url.trim());
if (result.success) {
setUrlResult(result);
// Set text based on user preference
const textToAnalyze = useArticleOnly ? result.articleText : result.allText;
setText(textToAnalyze);
setError('');
} else {
setError(result.error);
setUrlResult(null);
}
} catch (err) {
let errorMessage = err.message;
if (errorMessage.includes('Failed to fetch')) {
errorMessage = 'Unable to fetch content due to CORS restrictions or network issues';
}
setError(errorMessage);
setUrlResult(null);
} finally {
setIsLoading(false);
}
};
const clearText = () => {
setText('');
setUrlResult(null);
setError('');
};
const clearUrl = () => {
setUrl('');
setUrlResult(null);
setError('');
};
const loadSample = () => {
@@ -101,6 +151,121 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
description="Analyze text length, word count, and other text statistics"
icon={Type}
>
{/* URL Input Section */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 mb-6">
<div className="flex items-center gap-2 mb-3">
<Globe className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Analyze Content from URL</h3>
</div>
<div className="space-y-3">
<div className="flex gap-2">
<div className="relative flex-1">
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com/article"
className="tool-input w-full pr-10"
disabled={isLoading}
/>
{url && !isLoading && (
<button
onClick={clearUrl}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
>
<X className="h-4 w-4" />
</button>
)}
</div>
<button
onClick={fetchUrlContent}
disabled={isLoading || !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"
>
{isLoading ? (
<>
<Clock className="h-4 w-4 mr-2 animate-spin" />
Fetching...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
Fetch Content
</>
)}
</button>
</div>
{/* URL Result Status */}
{urlResult && (
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
<div className="flex items-start gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">{CONTENT_TYPE_INFO[urlResult.contentType].emoji}</span>
<span className={`font-medium ${CONTENT_TYPE_INFO[urlResult.contentType].color}`}>
{CONTENT_TYPE_INFO[urlResult.contentType].label}
</span>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
{CONTENT_TYPE_INFO[urlResult.contentType].description}
</div>
{urlResult.title && (
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
{urlResult.title}
</div>
)}
<div className="text-xs text-gray-500 dark:text-gray-400">
Article: {urlResult.metrics.articleWordCount} words
Total: {urlResult.metrics.totalWordCount} words
Ratio: {Math.round(urlResult.metrics.contentRatio * 100)}%
</div>
</div>
<div className="flex items-center gap-2">
<label className="flex items-center text-sm">
<input
type="checkbox"
checked={useArticleOnly}
onChange={(e) => {
setUseArticleOnly(e.target.checked);
const textToAnalyze = e.target.checked ? urlResult.articleText : urlResult.allText;
setText(textToAnalyze);
}}
className="mr-2"
/>
Article Only
</label>
</div>
</div>
</div>
)}
{/* Error Display */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
<div className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<div className="text-sm text-red-700 dark:text-red-300 mb-2">{error}</div>
{error.includes('fetch') && (
<div className="text-xs text-red-600 dark:text-red-400">
<p className="mb-1"><strong>Common solutions:</strong></p>
<ul className="list-disc list-inside space-y-1">
<li>Some websites block cross-origin requests for security</li>
<li>Try copying the article text manually and pasting it below</li>
<li>The site might require JavaScript to load content</li>
<li>Check if the URL is accessible and returns HTML content</li>
</ul>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-3 mb-6">
<button onClick={loadSample} className="tool-button-secondary">
@@ -138,9 +303,32 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
{/* Statistics */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Text Statistics
</h3>
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Text Statistics
</h3>
{(stats.characters > 0 || stats.words > 0) && (
<button
onClick={() => {
const statsText = `Text Statistics:
Characters: ${formatNumber(stats.characters)}
Characters (no spaces): ${formatNumber(stats.charactersNoSpaces)}
Words: ${formatNumber(stats.words)}
Lines: ${formatNumber(stats.lines)}
Sentences: ${formatNumber(stats.sentences)}
Paragraphs: ${formatNumber(stats.paragraphs)}
Bytes: ${formatNumber(stats.bytes)}
${stats.words > 0 ? `Reading time: ${getReadingTime()}
Typing time: ${getTypingTime()}` : ''}`;
navigator.clipboard.writeText(statsText);
}}
className="flex items-center space-x-2 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<Copy className="h-4 w-4" />
<span>Copy Statistics</span>
</button>
)}
</div>
{/* Main Stats Grid */}
<div className="grid grid-cols-2 gap-4">
@@ -220,30 +408,6 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
</div>
)}
{/* Copy Statistics */}
{(stats.characters > 0 || stats.words > 0) && (
<div className="mt-4">
<button
onClick={() => {
const statsText = `Text Statistics:
Characters: ${formatNumber(stats.characters)}
Characters (no spaces): ${formatNumber(stats.charactersNoSpaces)}
Words: ${formatNumber(stats.words)}
Lines: ${formatNumber(stats.lines)}
Sentences: ${formatNumber(stats.sentences)}
Paragraphs: ${formatNumber(stats.paragraphs)}
Bytes: ${formatNumber(stats.bytes)}
${stats.words > 0 ? `Reading time: ${getReadingTime()}
Typing time: ${getTypingTime()}` : ''}`;
navigator.clipboard.writeText(statsText);
}}
className="flex items-center space-x-2 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<Copy className="h-4 w-4" />
<span>Copy Statistics</span>
</button>
</div>
)}
</div>
</div>
@@ -251,11 +415,13 @@ Typing time: ${getTypingTime()}` : ''}`;
<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> Perfect for checking character limits for social media posts, essays, or articles</li>
<li> Real-time counting updates as you type or paste text</li>
<li> Includes reading and typing time estimates based on average speeds</li>
<li> Byte count shows the actual storage size of your text in UTF-8 encoding</li>
<li> Use "Show Details" to see additional statistics like sentences and paragraphs</li>
<li> <strong>URL Analysis:</strong> Fetch and analyze content from any web page or article</li>
<li> <strong>Smart Content Detection:</strong> Automatically detects articles vs general web content</li>
<li> <strong>Article vs Full Page:</strong> Choose to analyze just the main article or entire page content</li>
<li> <strong>Real-time Counting:</strong> Updates as you type or paste text</li>
<li> <strong>Reading Time:</strong> Estimates based on average reading speed (225 WPM)</li>
<li> <strong>Content Quality:</strong> Shows content-to-noise ratio for web pages</li>
<li> Perfect for checking character limits for social media, essays, or articles</li>
</ul>
</div>
</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;
}

140
src/utils/analytics.js Normal file
View File

@@ -0,0 +1,140 @@
// Google Analytics utility for React SPA
// Implements best practices for Single Page Applications
// Google Analytics configuration
const GA_MEASUREMENT_ID = 'G-S3K5P2PWV6';
// Initialize Google Analytics with Consent Mode v2
export const initGA = () => {
// Don't initialize if already loaded
if (window.gtag) {
return;
}
// Show different behavior in development vs production
const isDevelopment = process.env.NODE_ENV !== 'production';
// Initialize gtag function first (required for Consent Mode)
window.dataLayer = window.dataLayer || [];
function gtag() {
window.dataLayer.push(arguments);
}
window.gtag = gtag;
// Initialize Consent Mode v2 BEFORE loading GA script
const { initConsentMode, applyStoredConsent } = require('./consentManager');
initConsentMode();
// Create script elements
const gtagScript = document.createElement('script');
gtagScript.async = true;
gtagScript.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`;
document.head.appendChild(gtagScript);
// Configure Google Analytics after script loads
gtagScript.onload = () => {
gtag('js', new Date());
gtag('config', GA_MEASUREMENT_ID, {
// SPA-specific configurations
send_page_view: false, // We'll manually send page views
anonymize_ip: true, // Privacy-first approach
allow_google_signals: false, // Disable advertising features for privacy
allow_ad_personalization_signals: false, // Disable ad personalization
// Development mode settings
debug_mode: isDevelopment,
});
applyStoredConsent();
};
};
// Track page views
export const trackPageView = (path, title) => {
if (!window.gtag) {
return;
}
window.gtag('config', GA_MEASUREMENT_ID, {
page_path: path,
page_title: title,
});
};
// Track custom events
export const trackEvent = (eventName, parameters = {}) => {
if (!window.gtag) {
return;
}
window.gtag('event', eventName, {
...parameters,
// Add privacy-friendly defaults
anonymize_ip: true,
});
};
// Predefined events for common actions
export const trackToolUsage = (toolName, action = 'use') => {
trackEvent('tool_interaction', {
tool_name: toolName,
action: action,
event_category: 'tools',
});
};
export const trackSearch = (searchTerm) => {
// Only track that a search happened, not the actual term for privacy
trackEvent('search', {
event_category: 'engagement',
// Don't send the actual search term for privacy
has_results: searchTerm.length > 0,
});
};
export const trackThemeChange = (theme) => {
trackEvent('theme_change', {
theme: theme,
event_category: 'preferences',
});
};
export const trackError = (errorType, errorMessage) => {
trackEvent('exception', {
description: `${errorType}: ${errorMessage}`,
fatal: false,
event_category: 'errors',
});
};
// Check if user has opted out of analytics
export const isAnalyticsEnabled = () => {
// Check for common opt-out methods
if (navigator.doNotTrack === '1' ||
window.doNotTrack === '1' ||
navigator.msDoNotTrack === '1') {
return false;
}
// Check for ad blockers or analytics blockers
if (!window.gtag && process.env.NODE_ENV === 'production') {
return false;
}
return true;
};
// Privacy-friendly analytics info
export const getAnalyticsInfo = () => {
return {
enabled: isAnalyticsEnabled(),
measurementId: GA_MEASUREMENT_ID,
environment: process.env.NODE_ENV,
privacyFeatures: {
anonymizeIp: true,
disableAdvertising: true,
disablePersonalization: true,
clientSideOnly: true,
}
};
};

144
src/utils/browserCompat.js Normal file
View File

@@ -0,0 +1,144 @@
// Browser compatibility utilities for handling different browser environments
/**
* Detect if the app is running in Telegram's built-in browser
*/
export const isTelegramBrowser = () => {
const userAgent = navigator.userAgent.toLowerCase();
return userAgent.includes('telegram') ||
userAgent.includes('tgios') ||
userAgent.includes('tgandroid') ||
// Check for Telegram-specific window properties
(window.TelegramWebviewProxy !== undefined) ||
// Check for common Telegram browser characteristics
(userAgent.includes('mobile') && userAgent.includes('webkit') && !userAgent.includes('chrome'));
};
/**
* Detect if the app is running in any mobile in-app browser
*/
export const isInAppBrowser = () => {
const userAgent = navigator.userAgent.toLowerCase();
return userAgent.includes('wv') || // WebView
userAgent.includes('telegram') ||
userAgent.includes('fbav') || // Facebook
userAgent.includes('fban') || // Facebook
userAgent.includes('instagram') ||
userAgent.includes('twitter') ||
userAgent.includes('line') ||
userAgent.includes('whatsapp');
};
/**
* Get browser information
*/
export const getBrowserInfo = () => {
const userAgent = navigator.userAgent;
return {
userAgent,
isTelegram: isTelegramBrowser(),
isInApp: isInAppBrowser(),
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent),
isIOS: /iPad|iPhone|iPod/.test(userAgent),
isAndroid: /Android/.test(userAgent)
};
};
/**
* Add polyfills and compatibility fixes for problematic browsers
*/
export const addCompatibilityFixes = () => {
// Fix for missing or problematic console methods in some browsers
if (!window.console) {
window.console = {
log: () => {},
error: () => {},
warn: () => {},
info: () => {},
debug: () => {}
};
}
// Ensure console methods exist and are functions
['log', 'error', 'warn', 'info', 'debug'].forEach(method => {
if (typeof console[method] !== 'function') {
console[method] = () => {};
}
});
// Add requestAnimationFrame polyfill if missing
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = (callback) => {
return setTimeout(callback, 1000 / 60);
};
}
// Add cancelAnimationFrame polyfill if missing
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = (id) => {
clearTimeout(id);
};
}
// Fix for missing or problematic localStorage in some browsers
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
} catch (e) {
window.localStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => null
};
}
// Fix for missing or problematic sessionStorage
try {
sessionStorage.setItem('test', 'test');
sessionStorage.removeItem('test');
} catch (e) {
window.sessionStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => null
};
}
};
/**
* Initialize compatibility fixes
*/
export const initBrowserCompat = () => {
const browserInfo = getBrowserInfo();
// Add compatibility fixes
addCompatibilityFixes();
// Add specific fixes for Telegram browser
if (browserInfo.isTelegram) {
console.log('Telegram browser detected - applying compatibility fixes');
// Add Telegram-specific error handling
window.addEventListener('error', (event) => {
console.log('Global error caught in Telegram browser:', event.error);
// Prevent the error from bubbling up and showing the error overlay
event.preventDefault();
return true;
});
window.addEventListener('unhandledrejection', (event) => {
console.log('Unhandled promise rejection in Telegram browser:', event.reason);
// Prevent the error from bubbling up
event.preventDefault();
return true;
});
}
return browserInfo;
};

203
src/utils/consentManager.js Normal file
View File

@@ -0,0 +1,203 @@
// GDPR Consent Management with Google Consent Mode v2
// Implements TCF 2.2 compatible consent management
// Consent categories
export const CONSENT_CATEGORIES = {
NECESSARY: 'necessary',
ANALYTICS: 'analytics_storage',
ADVERTISING: 'ad_storage',
PERSONALIZATION: 'ad_personalization',
USER_DATA: 'ad_user_data'
};
// Default consent state (denied until user consents)
const DEFAULT_CONSENT = {
[CONSENT_CATEGORIES.NECESSARY]: 'granted', // Always granted for essential functionality
[CONSENT_CATEGORIES.ANALYTICS]: 'denied',
[CONSENT_CATEGORIES.ADVERTISING]: 'denied',
[CONSENT_CATEGORIES.PERSONALIZATION]: 'denied',
[CONSENT_CATEGORIES.USER_DATA]: 'denied'
};
// Check if user is in EEA (European Economic Area)
export const isEEAUser = () => {
// Simple timezone-based detection (not 100% accurate but good enough)
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const eeaTimezones = [
'Europe/', 'Atlantic/Reykjavik', 'Atlantic/Faroe', 'Atlantic/Canary',
'Africa/Ceuta', 'Arctic/Longyearbyen'
];
return eeaTimezones.some(tz => timezone.startsWith(tz));
};
// Initialize Google Consent Mode
export const initConsentMode = () => {
if (typeof window === 'undefined') return;
// Initialize gtag if not already available
if (!window.gtag) {
window.dataLayer = window.dataLayer || [];
function gtag() {
window.dataLayer.push(arguments);
}
window.gtag = gtag;
}
const isEEA = isEEAUser();
if (isEEA) {
// EEA users: Start with denied, wait for user consent
window.gtag('consent', 'default', {
...DEFAULT_CONSENT,
region: ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'IS', 'LI', 'NO'],
wait_for_update: 500
});
} else {
// Non-EEA users: Automatically grant all consent
window.gtag('consent', 'default', {
...CONSENT_CONFIGS.ACCEPT_ALL,
region: ['US'],
wait_for_update: 0 // No need to wait
});
}
};
// Update consent based on user choice
export const updateConsent = (consentChoices) => {
if (typeof window === 'undefined' || !window.gtag) return;
window.gtag('consent', 'update', consentChoices);
// Store consent in localStorage
localStorage.setItem('consent_preferences', JSON.stringify({
...consentChoices,
timestamp: Date.now()
}));
};
// Get stored consent preferences
export const getStoredConsent = () => {
try {
const stored = localStorage.getItem('consent_preferences');
if (stored) {
const parsed = JSON.parse(stored);
// Check if consent is less than 1 year old
if (Date.now() - parsed.timestamp < 365 * 24 * 60 * 60 * 1000) {
return parsed;
}
}
} catch (error) {
console.error('Error reading stored consent:', error);
}
return null;
};
// Check if consent banner should be shown
export const shouldShowConsentBanner = () => {
const isDev = process.env.NODE_ENV !== 'production';
const isEEA = isEEAUser();
const hasConsent = getStoredConsent();
// In development, only show for EEA users (for proper testing)
if (isDev) {
if (isEEA) {
return !hasConsent;
} else {
// Auto-grant for non-EEA even in development
if (!hasConsent) {
updateConsent(CONSENT_CONFIGS.ACCEPT_ALL);
}
return false;
}
}
// In production, only show for EEA users who haven't consented
if (isEEA) {
return !hasConsent;
}
// For non-EEA users, automatically grant all consent and never show banner
if (!hasConsent) {
updateConsent(CONSENT_CONFIGS.ACCEPT_ALL);
}
return false; // Never show banner for non-EEA users
};
// Predefined consent configurations
export const CONSENT_CONFIGS = {
// Accept all (for users who want full functionality)
ACCEPT_ALL: {
[CONSENT_CATEGORIES.NECESSARY]: 'granted',
[CONSENT_CATEGORIES.ANALYTICS]: 'granted',
[CONSENT_CATEGORIES.ADVERTISING]: 'granted',
[CONSENT_CATEGORIES.PERSONALIZATION]: 'granted',
[CONSENT_CATEGORIES.USER_DATA]: 'granted'
},
// Essential only (minimal consent)
ESSENTIAL_ONLY: {
[CONSENT_CATEGORIES.NECESSARY]: 'granted',
[CONSENT_CATEGORIES.ANALYTICS]: 'denied',
[CONSENT_CATEGORIES.ADVERTISING]: 'denied',
[CONSENT_CATEGORIES.PERSONALIZATION]: 'denied',
[CONSENT_CATEGORIES.USER_DATA]: 'denied'
},
// Analytics only (for users who want to help improve the service)
ANALYTICS_ONLY: {
[CONSENT_CATEGORIES.NECESSARY]: 'granted',
[CONSENT_CATEGORIES.ANALYTICS]: 'granted',
[CONSENT_CATEGORIES.ADVERTISING]: 'denied',
[CONSENT_CATEGORIES.PERSONALIZATION]: 'denied',
[CONSENT_CATEGORIES.USER_DATA]: 'denied'
}
};
// Apply stored consent on page load
export const applyStoredConsent = () => {
const stored = getStoredConsent();
if (stored && window.gtag) {
const { timestamp, version, ...consentChoices } = stored;
window.gtag('consent', 'update', consentChoices);
}
};
// Consent banner component data
export const getConsentBannerData = () => {
return {
title: 'We respect your privacy',
description: 'We use cookies and similar technologies to improve your experience, analyze site usage, and assist in our marketing efforts. Your data stays private with our client-side tools.',
purposes: [
{
id: CONSENT_CATEGORIES.NECESSARY,
name: 'Essential',
description: 'Required for basic site functionality',
required: true
},
{
id: CONSENT_CATEGORIES.ANALYTICS,
name: 'Analytics',
description: 'Help us understand how you use our tools (Google Analytics)',
required: false
},
{
id: CONSENT_CATEGORIES.ADVERTISING,
name: 'Advertising',
description: 'Future ad personalization (not yet implemented)',
required: false
}
],
buttons: {
acceptAll: 'Accept All',
essentialOnly: 'Essential Only',
customize: 'Customize',
save: 'Save Preferences'
},
links: {
privacy: '/privacy',
terms: '/terms'
}
};
};

View File

@@ -0,0 +1,371 @@
// Content extraction and article detection utilities
/**
* Content classification types
*/
export const CONTENT_TYPES = {
RICH_ARTICLE: 'rich_article',
GENERAL_CONTENT: 'general_content',
LIMITED_CONTENT: 'limited_content',
NO_CONTENT: 'no_content'
};
/**
* Content type display information
*/
export const CONTENT_TYPE_INFO = {
[CONTENT_TYPES.RICH_ARTICLE]: {
label: 'Rich Article Content',
emoji: '🟢',
description: 'Clear article structure with headings and paragraphs',
color: 'text-green-600 dark:text-green-400'
},
[CONTENT_TYPES.GENERAL_CONTENT]: {
label: 'General Web Content',
emoji: '🟡',
description: 'Readable text mixed with navigation and UI elements',
color: 'text-yellow-600 dark:text-yellow-400'
},
[CONTENT_TYPES.LIMITED_CONTENT]: {
label: 'Limited Text Content',
emoji: '🟠',
description: 'Mostly UI/navigation with minimal readable text',
color: 'text-orange-600 dark:text-orange-400'
},
[CONTENT_TYPES.NO_CONTENT]: {
label: 'No Readable Content',
emoji: '🔴',
description: 'Images, videos, or heavily JavaScript-dependent content',
color: 'text-red-600 dark:text-red-400'
}
};
/**
* CORS proxy services for fetching external content
*/
const CORS_PROXIES = [
'https://api.allorigins.win/get?url=',
'https://corsproxy.io/?',
'https://cors-anywhere.herokuapp.com/',
'https://thingproxy.freeboard.io/fetch/'
];
/**
* Fetch and parse HTML content from URL with CORS proxy fallback
*/
export const fetchUrlContent = async (url) => {
try {
// Validate URL
const urlObj = new URL(url);
if (!['http:', 'https:'].includes(urlObj.protocol)) {
throw new Error('Only HTTP and HTTPS URLs are supported');
}
// First try direct fetch (works for same-origin or CORS-enabled sites)
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'User-Agent': 'Mozilla/5.0 (compatible; TextAnalyzer/1.0)'
}
});
if (response.ok) {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('text/html')) {
const html = await response.text();
return { html, url: response.url, contentType };
}
}
} catch (directError) {
console.log('Direct fetch failed, trying CORS proxy:', directError.message);
}
// Try CORS proxies
let lastError = null;
for (const proxy of CORS_PROXIES) {
try {
let proxyUrl;
let response;
if (proxy.includes('allorigins.win')) {
// AllOrigins returns JSON with contents
proxyUrl = `${proxy}${encodeURIComponent(url)}`;
response = await fetch(proxyUrl);
if (response.ok) {
const data = await response.json();
if (data.contents) {
return {
html: data.contents,
url: data.status.url || url,
contentType: 'text/html'
};
}
}
} else {
// Other proxies return HTML directly
proxyUrl = `${proxy}${url}`;
response = await fetch(proxyUrl, {
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
});
if (response.ok) {
const contentType = response.headers.get('content-type') || 'text/html';
if (contentType.includes('text/html') || contentType.includes('text/plain')) {
const html = await response.text();
return { html, url, contentType };
}
}
}
} catch (proxyError) {
lastError = proxyError;
console.log(`Proxy ${proxy} failed:`, proxyError.message);
continue;
}
}
throw new Error(`All fetch methods failed. Last error: ${lastError?.message || 'Unknown error'}`);
} catch (error) {
throw new Error(`Failed to fetch content: ${error.message}`);
}
};
/**
* Parse HTML and create DOM
*/
export const parseHtml = (html) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
return doc;
};
/**
* Detect article elements and structure
*/
export const detectArticleStructure = (doc) => {
const structure = {
hasArticleTag: false,
hasMainTag: false,
headingCount: 0,
paragraphCount: 0,
hasMetaArticle: false,
hasJsonLd: false,
wordCount: 0,
linkDensity: 0
};
// Check for semantic HTML5 tags
structure.hasArticleTag = doc.querySelector('article') !== null;
structure.hasMainTag = doc.querySelector('main') !== null;
// Count headings
structure.headingCount = doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
// Count paragraphs
structure.paragraphCount = doc.querySelectorAll('p').length;
// Check meta tags for articles
const metaTags = doc.querySelectorAll('meta[property^="og:"], meta[name^="article:"]');
structure.hasMetaArticle = Array.from(metaTags).some(meta =>
(meta.getAttribute('property') === 'og:type' && meta.getAttribute('content') === 'article') ||
meta.getAttribute('name')?.startsWith('article:')
);
// Check for JSON-LD structured data
const jsonLdScripts = doc.querySelectorAll('script[type="application/ld+json"]');
structure.hasJsonLd = Array.from(jsonLdScripts).some(script => {
try {
const data = JSON.parse(script.textContent);
const type = data['@type'] || (Array.isArray(data) ? data[0]['@type'] : null);
return type && ['Article', 'NewsArticle', 'BlogPosting'].includes(type);
} catch {
return false;
}
});
return structure;
};
/**
* Extract clean text from article elements
*/
export const extractArticleText = (doc) => {
const articleSelectors = [
'article',
'main article',
'[role="main"] article',
'.article-content',
'.post-content',
'.entry-content',
'.content-body'
];
// Try to find article container
let articleContainer = null;
for (const selector of articleSelectors) {
articleContainer = doc.querySelector(selector);
if (articleContainer) break;
}
// If no article container, try main content area
if (!articleContainer) {
const mainSelectors = ['main', '[role="main"]', '#main', '#content', '.main-content'];
for (const selector of mainSelectors) {
articleContainer = doc.querySelector(selector);
if (articleContainer) break;
}
}
// Extract text from container or full document
const container = articleContainer || doc.body;
if (!container) return { text: '', elements: [] };
// Remove unwanted elements
const unwantedSelectors = [
'script', 'style', 'nav', 'header', 'footer', 'aside',
'.navigation', '.nav', '.menu', '.sidebar', '.ads', '.advertisement',
'.social-share', '.comments', '.related-posts', '.author-bio'
];
const clone = container.cloneNode(true);
unwantedSelectors.forEach(selector => {
clone.querySelectorAll(selector).forEach(el => el.remove());
});
// Extract text from meaningful elements
const meaningfulElements = clone.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, blockquote, pre');
const elements = Array.from(meaningfulElements).map(el => ({
tag: el.tagName.toLowerCase(),
text: el.textContent.trim(),
length: el.textContent.trim().length
})).filter(el => el.length > 0);
const text = elements.map(el => el.text).join('\n\n');
return { text, elements };
};
/**
* Extract all visible text from page
*/
export const extractAllText = (doc) => {
const clone = doc.body.cloneNode(true);
// Remove unwanted elements
const unwantedSelectors = ['script', 'style', 'noscript'];
unwantedSelectors.forEach(selector => {
clone.querySelectorAll(selector).forEach(el => el.remove());
});
const text = clone.textContent || clone.innerText || '';
return text.replace(/\s+/g, ' ').trim();
};
/**
* Calculate content quality metrics
*/
export const calculateContentMetrics = (doc, articleText, allText) => {
const metrics = {
articleWordCount: articleText.split(/\s+/).filter(w => w.length > 0).length,
totalWordCount: allText.split(/\s+/).filter(w => w.length > 0).length,
contentRatio: 0,
linkCount: doc.querySelectorAll('a[href]').length,
imageCount: doc.querySelectorAll('img').length,
headingCount: doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length,
paragraphCount: doc.querySelectorAll('p').length,
linkDensity: 0
};
if (metrics.totalWordCount > 0) {
metrics.contentRatio = metrics.articleWordCount / metrics.totalWordCount;
metrics.linkDensity = metrics.linkCount / metrics.totalWordCount;
}
return metrics;
};
/**
* Classify content type based on structure and metrics
*/
export const classifyContent = (structure, metrics, articleText) => {
const wordCount = metrics.articleWordCount;
const contentRatio = metrics.contentRatio;
const hasStructure = structure.hasArticleTag || structure.hasMainTag || structure.hasMetaArticle;
const hasGoodStructure = structure.headingCount >= 2 && structure.paragraphCount >= 3;
// Rich Article Content
if ((hasStructure || hasGoodStructure) && wordCount >= 300 && contentRatio > 0.6) {
return CONTENT_TYPES.RICH_ARTICLE;
}
// General Web Content
if (wordCount >= 100 && contentRatio > 0.3) {
return CONTENT_TYPES.GENERAL_CONTENT;
}
// Limited Content
if (wordCount >= 20) {
return CONTENT_TYPES.LIMITED_CONTENT;
}
// No readable content
return CONTENT_TYPES.NO_CONTENT;
};
/**
* Main function to extract and analyze content from URL
*/
export const extractContentFromUrl = async (url) => {
try {
// Fetch content
const { html, url: finalUrl } = await fetchUrlContent(url);
// Parse HTML
const doc = parseHtml(html);
// Detect article structure
const structure = detectArticleStructure(doc);
// Extract text content
const { text: articleText, elements } = extractArticleText(doc);
const allText = extractAllText(doc);
// Calculate metrics
const metrics = calculateContentMetrics(doc, articleText, allText);
// Classify content
const contentClassification = classifyContent(structure, metrics, articleText);
// Get page metadata
const title = doc.querySelector('title')?.textContent?.trim() || '';
const description = doc.querySelector('meta[name="description"]')?.getAttribute('content') || '';
return {
success: true,
url: finalUrl,
title,
description,
contentType: contentClassification,
structure,
metrics,
articleText,
allText,
elements,
extractedAt: new Date().toISOString()
};
} catch (error) {
return {
success: false,
error: error.message,
url
};
}
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,262 @@
/**
* Release Notes API Integration
*
* This file provides multiple options for making the "What's New" feature dynamic:
* 1. GitHub API integration (recommended)
* 2. GitLab API integration
* 3. Custom backend API
* 4. Static JSON file approach
*/
// Option 1: GitHub API Integration (Recommended)
export const fetchGitHubReleases = async (owner, repo, token = null) => {
try {
const headers = {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'DevTools-App'
};
// Add token if provided (for private repos or higher rate limits)
if (token) {
headers['Authorization'] = `token ${token}`;
}
// Fetch commits from GitHub API
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/commits?per_page=20`,
{
method: 'GET',
headers
}
);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const commits = await response.json();
return commits.map(commit => ({
id: commit.sha,
message: commit.commit.message,
date: commit.commit.author.date,
author: commit.commit.author.name,
url: commit.html_url
}));
} catch (error) {
console.error('Failed to fetch GitHub releases:', error);
return [];
}
};
// Option 2: Gitea API Integration (for your Coolify server setup)
export const fetchGiteaReleases = async (owner, repo, token, baseUrl) => {
try {
// Use URL parameters for auth to avoid CORS preflight
const url = new URL(`${baseUrl}/api/v1/repos/${owner}/${repo}/commits`);
url.searchParams.set('limit', '20');
if (token) {
url.searchParams.set('token', token);
}
// Fetch commits from Gitea API with minimal headers
const response = await fetch(url.toString(), {
method: 'GET',
mode: 'cors'
});
if (!response.ok) {
throw new Error(`Gitea API error: ${response.status}`);
}
const commits = await response.json();
return commits.map(commit => ({
id: commit.sha,
message: commit.commit.message,
date: commit.commit.author.date,
author: commit.commit.author.name,
url: `${baseUrl}/${owner}/${repo}/commit/${commit.sha}`
}));
} catch (error) {
console.error('Failed to fetch Gitea releases:', error);
return [];
}
};
// Option 3: Custom Backend API
export const fetchCustomReleases = async (apiEndpoint) => {
try {
const response = await fetch(apiEndpoint, { method: 'GET' });
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch custom releases:', error);
return [];
}
};
// Option 4: Static JSON File (simplest approach)
export const fetchStaticReleases = async () => {
try {
const response = await fetch('/data/releases.json', { method: 'GET' });
if (!response.ok) {
throw new Error(`Failed to load releases: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to fetch static releases:', error);
return [];
}
};
// Enhanced commit message parser with better categorization
export const parseCommitMessage = (message) => {
// Skip non-user-informative commits
const skipPatterns = [
/^fix eslint/i,
/^remove.*eslint/i,
/^update.*package/i,
/^add debug/i,
/^fix.*dependency/i,
/deployment/i,
/^fix.*mismatch/i,
/^merge/i,
/^wip/i,
/^temp/i
];
if (skipPatterns.some(pattern => pattern.test(message))) {
return null;
}
// Enhanced transformations with better pattern matching
const transformations = [
{
pattern: /feat.*invoice.*editor.*improvements/i,
type: 'feature',
title: 'Invoice Editor Major Update',
description: 'Complete overhaul of Invoice Editor with currency system, PDF generation fixes, improved UI/UX, removed print functionality (use PDF download instead), streamlined preview toolbar, and comprehensive bug fixes'
},
{
pattern: /feat.*enhanced.*what.*new.*feature.*non_tools.*category.*global.*footer/i,
type: 'feature',
title: 'What\'s New Feature & Navigation Improvements',
description: 'Added attractive "What\'s New" button to homepage, created NON_TOOLS category for better navigation organization, separated navigation items in sidebar and mobile menu, and implemented unified global footer across all pages'
},
{
pattern: /improve.*objecteditor.*postmantable.*ui\/ux/i,
type: 'enhancement',
title: 'Enhanced Object Editor & Table View',
description: 'Improved user interface and experience with better JSON parsing, HTML rendering, and copy functionality'
},
{
pattern: /feat.*analytics.*mobile.*ui/i,
type: 'feature',
title: 'Mobile UI Improvements',
description: 'Optimized interface for mobile devices with better analytics integration'
},
{
pattern: /feat.*seo.*gdpr/i,
type: 'feature',
title: 'SEO & Privacy Compliance',
description: 'Comprehensive SEO optimization with GDPR-compliant analytics and consent management'
},
{
pattern: /fix.*bug|bug.*fix/i,
type: 'fix',
title: 'Bug Fixes',
description: message.split('\n')[0].replace(/^(fix|bug):\s*/i, '')
},
{
pattern: /feat.*|add.*|new.*/i,
type: 'feature',
title: 'New Feature',
description: message.split('\n')[0].replace(/^feat:\s*/i, '')
},
{
pattern: /enhance.*|improve.*|update.*/i,
type: 'enhancement',
title: 'Enhancement',
description: message.split('\n')[0].replace(/^(enhance|improve|update):\s*/i, '')
},
{
pattern: /security.*|sec.*/i,
type: 'security',
title: 'Security Update',
description: message.split('\n')[0].replace(/^security:\s*/i, '')
}
];
for (const transform of transformations) {
if (transform.pattern.test(message)) {
return {
type: transform.type,
title: transform.title,
description: transform.description
};
}
}
// Default fallback
return {
type: 'enhancement',
title: 'Update',
description: message.split('\n')[0].substring(0, 100) + (message.length > 100 ? '...' : '')
};
};
// Main function to get releases from your preferred source
export const getReleases = async (config = {}) => {
const {
source = 'static', // 'github', 'gitea', 'custom', 'static'
owner = 'dwindown',
repo = 'dewedev',
token = null,
apiEndpoint = null,
baseUrl = null
} = config;
let rawCommits = [];
switch (source) {
case 'github':
rawCommits = await fetchGitHubReleases(owner, repo, token);
break;
case 'gitea':
rawCommits = await fetchGiteaReleases(owner, repo, token, baseUrl);
break;
case 'custom':
rawCommits = await fetchCustomReleases(apiEndpoint);
break;
case 'static':
default:
rawCommits = await fetchStaticReleases();
break;
}
// Process commits into release notes
const releases = rawCommits
.map(commit => {
const parsed = parseCommitMessage(commit.message);
if (!parsed) return null;
return {
id: commit.id,
date: commit.date,
author: commit.author,
url: commit.url,
...parsed
};
})
.filter(Boolean)
.slice(0, 10); // Limit to recent 10 releases
return releases;
};

256
src/utils/seo.js Normal file
View File

@@ -0,0 +1,256 @@
import { TOOLS, SITE_CONFIG } from '../config/tools';
// SEO metadata generator
export const generateSEOData = (path) => {
const baseUrl = SITE_CONFIG.domain;
const defaultTitle = `${SITE_CONFIG.title} - ${SITE_CONFIG.subtitle}`;
const defaultDescription = SITE_CONFIG.description;
// Find tool by path
const tool = TOOLS.find(t => t.path === path);
// Generate SEO data based on route
switch (path) {
case '/':
return {
title: defaultTitle,
description: `${SITE_CONFIG.totalTools} professional developer utilities. ${defaultDescription}. JSON editor, URL encoder, Base64 converter, code beautifier, and more.`,
keywords: 'developer tools, JSON editor, URL encoder, Base64 converter, code beautifier, text diff, web utilities, programming tools',
canonical: baseUrl,
ogType: 'website',
ogImage: `${baseUrl}/og-image.png`,
twitterCard: 'summary_large_image',
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: SITE_CONFIG.title,
description: defaultDescription,
url: baseUrl,
potentialAction: {
'@type': 'SearchAction',
target: `${baseUrl}/?search={search_term_string}`,
'query-input': 'required name=search_term_string'
},
publisher: {
'@type': 'Organization',
name: SITE_CONFIG.title,
url: baseUrl
}
}
};
case '/privacy':
return {
title: `Privacy Policy - ${SITE_CONFIG.title}`,
description: 'Our privacy-first approach to developer tools. Learn how we protect your data with 100% client-side processing and minimal analytics.',
keywords: 'privacy policy, data protection, client-side processing, developer tools privacy',
canonical: `${baseUrl}/privacy`,
ogType: 'article',
noindex: false,
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Privacy Policy',
description: 'Privacy policy for Dewe.Dev developer tools',
url: `${baseUrl}/privacy`,
isPartOf: {
'@type': 'WebSite',
name: SITE_CONFIG.title,
url: baseUrl
}
}
};
case '/terms':
return {
title: `Terms of Service - ${SITE_CONFIG.title}`,
description: 'Terms of service for using our developer tools. Professional-grade utilities with transparent policies.',
keywords: 'terms of service, developer tools terms, usage policy',
canonical: `${baseUrl}/terms`,
ogType: 'article',
noindex: false,
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Terms of Service',
description: 'Terms of service for Dewe.Dev developer tools',
url: `${baseUrl}/terms`,
isPartOf: {
'@type': 'WebSite',
name: SITE_CONFIG.title,
url: baseUrl
}
}
};
case '/release-notes':
return {
title: `Release Notes - ${SITE_CONFIG.title}`,
description: 'Latest updates, features, and improvements to our developer tools. Stay up-to-date with new releases and enhancements.',
keywords: 'release notes, updates, changelog, new features, developer tools updates',
canonical: `${baseUrl}/release-notes`,
ogType: 'article',
noindex: false,
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Release Notes',
description: 'Latest updates and release notes for Dewe.Dev developer tools',
url: `${baseUrl}/release-notes`,
isPartOf: {
'@type': 'WebSite',
name: SITE_CONFIG.title,
url: baseUrl
}
}
};
case '/invoice-preview':
return {
title: `Invoice Preview - ${SITE_CONFIG.title}`,
description: 'Preview and download your professional invoice with customizable templates.',
keywords: 'invoice preview, pdf generation, invoice templates, professional invoices',
canonical: `${baseUrl}/invoice-preview`,
ogType: 'website',
noindex: true, // Don't index preview pages
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: 'Invoice Preview',
description: 'Invoice preview and PDF generation tool',
url: `${baseUrl}/invoice-preview`,
isPartOf: {
'@type': 'WebSite',
name: SITE_CONFIG.title,
url: baseUrl
}
}
};
default:
if (tool) {
const toolKeywords = tool.tags.join(', ').toLowerCase();
return {
title: `${tool.name} - ${SITE_CONFIG.title}`,
description: `${tool.description}. Free online ${tool.name.toLowerCase()} tool. ${defaultDescription}.`,
keywords: `${toolKeywords}, ${tool.name.toLowerCase()}, developer tools, online tools, web utilities`,
canonical: `${baseUrl}${tool.path}`,
ogType: 'website',
ogImage: `${baseUrl}/og-tools.png`,
twitterCard: 'summary',
structuredData: {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: tool.name,
description: tool.description,
url: `${baseUrl}${tool.path}`,
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web Browser',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD'
},
publisher: {
'@type': 'Organization',
name: SITE_CONFIG.title,
url: baseUrl
},
isPartOf: {
'@type': 'WebSite',
name: SITE_CONFIG.title,
url: baseUrl
},
keywords: toolKeywords,
featureList: tool.tags
}
};
}
// Fallback for unknown routes
return {
title: `Page Not Found - ${SITE_CONFIG.title}`,
description: defaultDescription,
keywords: 'developer tools, web utilities',
canonical: `${baseUrl}${path}`,
ogType: 'website',
noindex: true
};
}
};
// Generate Open Graph meta tags
export const generateOGTags = (seoData) => {
return [
{ property: 'og:type', content: seoData.ogType || 'website' },
{ property: 'og:title', content: seoData.title },
{ property: 'og:description', content: seoData.description },
{ property: 'og:url', content: seoData.canonical },
{ property: 'og:site_name', content: SITE_CONFIG.title },
...(seoData.ogImage ? [{ property: 'og:image', content: seoData.ogImage }] : []),
{ property: 'og:locale', content: 'en_US' }
];
};
// Generate Twitter Card meta tags
export const generateTwitterTags = (seoData) => {
return [
{ name: 'twitter:card', content: seoData.twitterCard || 'summary' },
{ name: 'twitter:title', content: seoData.title },
{ name: 'twitter:description', content: seoData.description },
...(seoData.ogImage ? [{ name: 'twitter:image', content: seoData.ogImage }] : [])
];
};
// Generate all meta tags for a route
export const generateMetaTags = (path) => {
const seoData = generateSEOData(path);
const basicMeta = [
{ name: 'description', content: seoData.description },
{ name: 'keywords', content: seoData.keywords },
{ name: 'author', content: SITE_CONFIG.title },
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' },
{ name: 'robots', content: seoData.noindex ? 'noindex,nofollow' : 'index,follow' },
{ name: 'googlebot', content: seoData.noindex ? 'noindex,nofollow' : 'index,follow' }
];
const ogTags = generateOGTags(seoData);
const twitterTags = generateTwitterTags(seoData);
return {
title: seoData.title,
meta: [...basicMeta, ...ogTags, ...twitterTags],
link: [
{ rel: 'canonical', href: seoData.canonical }
],
structuredData: seoData.structuredData
};
};
// Core Web Vitals optimization hints
export const getCoreWebVitalsOptimizations = () => {
return {
// Largest Contentful Paint (LCP)
lcp: {
preloadCriticalResources: true,
optimizeImages: true,
removeRenderBlockingResources: true
},
// First Input Delay (FID)
fid: {
minimizeJavaScript: true,
useWebWorkers: false, // Not needed for our tools
optimizeEventHandlers: true
},
// Cumulative Layout Shift (CLS)
cls: {
setImageDimensions: true,
reserveSpaceForAds: true, // Important for future AdSense
avoidDynamicContent: true,
useTransforms: true
}
};
};

View File

@@ -0,0 +1,123 @@
import { TOOLS, SITE_CONFIG } from '../config/tools';
// Generate sitemap.xml content
export const generateSitemap = () => {
const baseUrl = SITE_CONFIG.domain;
const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
// Define all routes with their priorities and change frequencies
const routes = [
{
url: '/',
priority: '1.0',
changefreq: 'weekly',
lastmod: currentDate
},
// Tool pages
...TOOLS.map(tool => ({
url: tool.path,
priority: '0.8',
changefreq: 'monthly',
lastmod: currentDate
})),
// Legal pages
{
url: '/privacy',
priority: '0.3',
changefreq: 'yearly',
lastmod: currentDate
},
{
url: '/terms',
priority: '0.3',
changefreq: 'yearly',
lastmod: currentDate
}
];
// Generate XML sitemap
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
${routes.map(route => ` <url>
<loc>${baseUrl}${route.url}</loc>
<lastmod>${route.lastmod}</lastmod>
<changefreq>${route.changefreq}</changefreq>
<priority>${route.priority}</priority>
</url>`).join('\n')}
</urlset>`;
return sitemap;
};
// Generate robots.txt content
export const generateRobotsTxt = () => {
const baseUrl = SITE_CONFIG.domain;
return `# Robots.txt for ${baseUrl}
# Generated automatically
User-agent: *
Allow: /
# Sitemap location
Sitemap: ${baseUrl}/sitemap.xml
# Block any future admin or private routes
Disallow: /admin/
Disallow: /api/
Disallow: /.well-known/
# Allow all major search engines
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Slurp
Allow: /
User-agent: DuckDuckBot
Allow: /
# Crawl delay for politeness
Crawl-delay: 1`;
};
// Build-time sitemap generation script
export const buildSitemap = () => {
const fs = require('fs');
const path = require('path');
const publicDir = path.join(process.cwd(), 'public');
// Generate and write sitemap.xml
const sitemapContent = generateSitemap();
fs.writeFileSync(path.join(publicDir, 'sitemap.xml'), sitemapContent, 'utf8');
// Generate and write robots.txt
const robotsContent = generateRobotsTxt();
fs.writeFileSync(path.join(publicDir, 'robots.txt'), robotsContent, 'utf8');
};
// Runtime sitemap data for dynamic generation
export const getSitemapData = () => {
return {
routes: [
{ path: '/', priority: 1.0, changefreq: 'weekly' },
...TOOLS.map(tool => ({
path: tool.path,
priority: 0.8,
changefreq: 'monthly'
})),
{ path: '/privacy', priority: 0.3, changefreq: 'yearly' },
{ path: '/terms', priority: 0.3, changefreq: 'yearly' }
],
baseUrl: SITE_CONFIG.domain,
totalUrls: TOOLS.length + 3 // tools + home + privacy + terms
};
};

View File

@@ -22,7 +22,12 @@ module.exports = {
},
fontFamily: {
mono: ['JetBrains Mono', 'Monaco', 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Code', 'Droid Sans Mono', 'Courier New', 'monospace'],
}
},
maxWidth: {
'1/4': '25%',
'1/2': '50%',
'3/4': '75%',
}
},
},
plugins: [],

27
temp_export_tabs.js Normal file
View File

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