Compare commits

..

15 Commits

Author SHA1 Message Date
Dwindi Ramadhana
1047642909 fix(diagram): correct flowchart template edge connections to match diagram logic 2026-06-14 20:06:01 +07:00
Dwindi Ramadhana
8caf6fbba5 fix(diagram): add missing Copy icon import for DiagramEditor export section 2026-06-14 19:57:25 +07:00
Dwindi Ramadhana
3727ace366 fix(build): remove unused imports in DiagramEditor and ReactFlowEditor 2026-06-14 18:45:14 +07:00
Dwindi Ramadhana
81c399ab42 fix(build): add missing DiagramEditor and FullscreenAdBanner files to git tracking 2026-06-14 16:05:40 +07:00
Dwindi Ramadhana
518b0127d2 fix(diagram): resolve DOMURL undefined error causing build failure 2026-06-14 15:55:08 +07:00
Dwindi Ramadhana
deb2bf0b8a fix(ads): adjust internal iframe styles to minimize bottom gap 2026-06-14 15:43:42 +07:00
Dwindi Ramadhana
e4ccff4bbf chore: update TODO.md to mark Markdown Editor MVP tasks as completed 2026-06-14 13:30:37 +07:00
Dwindi Ramadhana
0b1cfbdabd fix(layout): remove global overflow-x-hidden to restore sticky sidebar behavior 2026-06-14 13:15:51 +07:00
Dwindi Ramadhana
c580a5f7b0 fix(markdown-editor): align max width and formatting across read and edit views 2026-06-14 12:35:18 +07:00
Dwindi Ramadhana
9232052508 chore(ads): update adsterra anti-adblock custom domain to downconvenientmagnetic 2026-06-14 12:32:16 +07:00
Dwindi Ramadhana
fcbfeb44f8 fix(build): remove unused import EyeOff to pass strict CI build 2026-06-14 11:52:19 +07:00
Dwindi Ramadhana
dd0b98e077 chore: remove metadata files created by SSD 2026-06-14 01:01:03 +07:00
Dwindi Ramadhana
dcba58c2b9 chore: ignore OS meta files and update changelog for WYSIWYG and Object Editor updates 2026-06-14 00:56:38 +07:00
Dwindi Ramadhana
7b3dce06ea refactor(markdown-editor): migrate to tiptap for WYSIWYG editing, standardize UI spacing, and update export engine 2026-06-14 00:54:01 +07:00
Dwindi Ramadhana
13e694aa82 feat: add multidimensional search, preview mode prioritization, and collapse/expand all to Object Editor 2026-06-13 20:11:07 +07:00
29 changed files with 5470 additions and 2485 deletions

Binary file not shown.

BIN
._package-lock.json generated

Binary file not shown.

Binary file not shown.

3
.gitignore vendored
View File

@@ -11,7 +11,6 @@ node_modules/
/backup
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
@@ -46,4 +45,6 @@ pids
*.swo
# OS generated files
.DS_Store
Thumbs.db
._*

View File

@@ -131,37 +131,36 @@ Build a comprehensive suite of developer tools with a focus on:
---
#### Priority 3: AdSense Integration 💵
**Status:** ⏳ In Progress (Awaiting approval)
#### Priority 3: Adsterra Integration 💵
**Status:** ✅ Completed
**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
1. Apply for Adsterra account
2. Add Adsterra anti-adblock script to `index.html` and components
3. Create ad units in Adsterra dashboard
4. Implement ad components with Adsterra code
5. Test ad display and responsiveness
**Ad Units Needed:**
- Desktop Sidebar 1 (300x250)
- Desktop Sidebar 2 (300x250)
- Desktop Sidebar 3 (300x250)
- Mobile Bottom Banner (320x50)
- 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)
- Add Privacy Policy page
- Add Terms of Service page
- Cookie consent banner
- GDPR compliance
---
### Phase 2: Content Expansion (Week 3-6)
#### Markdown Editor 📝
**Status:** ✅ Completed (October 22, 2025)
**Status:** ✅ Completed (June 14, 2026)
**Timeline:** 1-2 weeks
**Impact:** HIGH - Major new feature, attracts new users
@@ -170,29 +169,25 @@ Build a comprehensive suite of developer tools with a focus on:
- 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)
- Open Files (.md, .txt)
- **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
- Tiptap-powered WYSIWYG Rich Text Editor
- Fallback Raw Markdown CodeMirror Editor
- View modes: Read, Edit, Markdown
- Toolbar (Bold, Italic, Headers, Links, Images, Code, Lists, Tables)
- Word count & statistics
- **Preview:**
- Live rendering (marked + DOMPurify)
- Auto-generated HTML parsing
- 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
- Markdown (.md)
- HTML (.html)
- HTML Content Body
- Plain Text (.txt)
- PDF (.pdf) - via html2pdf
- DOCX (.docx) - via docx library
**Advanced Features (Post-MVP):**
- Tables support (GitHub-style)

368
TODO.md
View File

@@ -179,310 +179,168 @@
---
### 💵 Priority 3: AdSense Integration (1 day) - ⏳ IN PROGRESS
### 💵 Priority 3: Adsterra Integration (1 day) - ✅ COMPLETED
#### 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)
#### Adsterra Setup
- [x] Apply for Adsterra publisher account
- [x] Add website URL
- [x] Receive approval
#### 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
- [x] Create ad unit: Desktop Sidebar 1 (300x250)
- [x] Create ad unit: Desktop Sidebar 2 (300x250)
- [x] Create ad unit: Mobile Bottom Banner (320x50)
- [x] Copy ad unit codes
- [x] Request Anti-Adblock custom domain
#### 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({});`
- [x] Update `AdBlock.jsx` with Adsterra iframe code
- [x] Update `MobileAdBanner.jsx` with Adsterra iframe code
- [x] Update custom Anti-Adblock domain (`downconvenientmagnetic.com`)
#### 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
- [x] Test ad display on desktop
- [x] Test ad display on mobile
- [x] Verify ads load correctly
- [x] Check for console errors
- [x] 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
- [x] Monitor ad impressions
- [x] Monitor ad clicks
- [x] Track CTR (Click-Through Rate)
#### 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)
- [x] Create Privacy Policy page
- [x] Data collection disclosure
- [x] Cookie usage disclosure
- [x] Third-party services (Adsterra)
- [x] User rights (GDPR)
- [x] Create Terms of Service page
- [x] Acceptable use policy
- [x] Disclaimer
- [x] Limitation of liability
- [x] Add cookie consent banner
- [x] Add "Privacy Policy" link in footer
- [x] Add "Terms of Service" link in footer
---
## 📋 Phase 2: Content Expansion
### 📝 Markdown Editor - MVP (1-2 weeks) - ✅ COMPLETED (Oct 22, 2025)
### 📝 Markdown Editor - MVP (1-2 weeks) - ✅ COMPLETED
#### Planning
- [ ] Finalize feature list for MVP
- [ ] Design UI mockup (split view)
- [ ] Plan component structure
- [ ] Choose markdown parser (marked vs markdown-it)
- [ ] Plan export formats
- [x] Finalize feature list for MVP
- [x] Design UI mockup (WYSIWYG layout)
- [x] Plan component structure
- [x] Implement Tiptap integration
- [x] Plan export formats
#### Project Setup
- [ ] Create `MarkdownEditor.jsx` page
- [ ] Set up routing (`/markdown-editor`)
- [ ] Add to navigation menu
- [ ] Add to homepage tools list
- [x] Create `MarkdownEditor.jsx` page
- [x] Create `RichMarkdownEditor.js` component
- [x] Set up routing (`/markdown-editor`)
- [x] Add to navigation menu
- [x] 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
- [x] Implement Create New tab
- [x] Start Empty button
- [x] Load Sample button (with example markdown)
- [x] Tip box
- [x] Implement URL tab
- [x] Use AdvancedURLFetch component
- [x] Support GitHub raw URLs
- [x] Support Gist URLs
- [x] Test with various markdown sources
- [x] Implement Paste tab
- [x] CodeMirror editor
- [x] Markdown syntax highlighting
- [x] Parse button
- [x] Implement Open tab
- [x] Support .md files
- [x] Support .txt files
- [x] 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
- [x] Implement WYSIWYG Editor (Tiptap)
- [x] Install `@tiptap/react` and `tiptap-markdown`
- [x] Add standard text formatting (bold, italic, strike)
- [x] Add block formatting (headers, quotes, lists)
- [x] Add inline code and code block extensions
- [x] Set up Lowlight syntax highlighting
- [x] Implement view mode toggle
- [x] Read mode (Clean preview default)
- [x] Edit mode (WYSIWYG Tiptap)
- [x] Markdown mode (Raw CodeMirror)
- [x] Fullscreen mode
- [x] Add editor features
- [x] Word count
- [x] Character count
- [x] Line count
- [x] 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
- [x] Build robust HTML to Markdown / Markdown to HTML sync
- [x] Set up markdown fallback parser (marked)
- [x] GitHub Flavored Markdown support (Tables, task lists)
- [x] Custom code block rendering with Copy button in Read mode
#### 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
- [x] Create collapsible export section
- [x] Implement Markdown export
- [x] Copy to clipboard
- [x] Download as .md file
- [x] Implement HTML export
- [x] Standalone HTML with CSS
- [x] Download as .html file
- [x] Implement HTML Content export
- [x] Strip React/Tailwind wrapper classes
- [x] Download body HTML only
- [x] Implement Plain Text export
- [x] Strip markdown syntax via regex
- [x] Download as .txt file
- [x] Implement PDF export
- [x] Install html2pdf.js
- [x] Inject CSS print media rules to prevent pre overflow
- [x] Download as .pdf file
#### Data Loss Prevention
- [ ] Implement hasUserData() function
- [ ] Implement hasModifiedData() function
- [ ] Add confirmation modal for tab changes
- [ ] Add confirmation for Create New buttons
- [x] Implement `hasUserData()` function
- [x] Implement `hasModifiedData()` function
- [x] Add confirmation modal for tab changes
- [x] 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)
- [x] Test all input methods
- [x] Test Tiptap to Markdown serialization
- [x] Test all export formats
- [x] Test code syntax highlighting
- [x] Test view mode toggle
- [x] Test toolbar buttons
- [x] Test responsive design
- [x] Test dark mode
---
### 📝 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 emoji support (WYSIWYG picker)
- [ ] 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
- [ ] Add mermaid diagram rendering
- [ ] Implement Table of Contents auto-generation
#### 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)
---

2425
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,22 +18,37 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/typography": "^0.5.20",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@tiptap/extension-code-block-lowlight": "^3.26.1",
"@tiptap/extension-image": "^3.26.1",
"@tiptap/extension-link": "^3.26.1",
"@tiptap/extension-table": "^3.26.1",
"@tiptap/extension-table-cell": "^3.26.1",
"@tiptap/extension-table-header": "^3.26.1",
"@tiptap/extension-table-row": "^3.26.1",
"@tiptap/extension-task-item": "^3.26.1",
"@tiptap/extension-task-list": "^3.26.1",
"@tiptap/react": "^3.26.1",
"@tiptap/starter-kit": "^3.26.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",
"html-to-image": "^1.11.13",
"html2pdf.js": "^0.12.1",
"js-beautify": "^1.15.4",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"lowlight": "^3.3.0",
"lucide-react": "^0.540.0",
"marked": "^16.4.1",
"marked-emoji": "^2.0.1",
"mermaid": "^11.15.0",
"papaparse": "^5.5.3",
"react": "18.3.1",
"react-diff-view": "^3.3.2",
@@ -41,9 +56,12 @@
"react-helmet-async": "^2.0.5",
"react-router-dom": "6.26.2",
"react-snap": "^1.23.0",
"react-zoom-pan-pinch": "^4.0.3",
"reactflow": "^11.11.4",
"serialize-javascript": "^6.0.0",
"serve": "^14.2.4",
"tailwindcss-typography": "^3.1.0",
"tiptap-markdown": "^0.9.0",
"turndown": "^7.2.1",
"web-vitals": "^2.1.4"
},

View File

@@ -1,5 +1,22 @@
{
"changelog": [
{
"date": "2026-06-14",
"changes": [
{
"datetime": "2026-06-14T10:00:00+07:00",
"type": "feature",
"title": "Major Markdown Editor Rewrite: WYSIWYG Experience",
"description": "Completely rebuilt the Markdown Editor to feature a true WYSIWYG (What You See Is What You Get) interface using Tiptap. You can now edit rich text visually like a Word document, while seamlessly converting back and forth to raw Markdown and clean HTML."
},
{
"datetime": "2026-06-14T09:30:00+07:00",
"type": "enhancement",
"title": "Object Editor: Data Preview & Multidimensional Search",
"description": "The Object Editor now defaults to a fast Read-Only preview when you paste JSON data. We also added an incredibly powerful multidimensional search bar that instantly filters, highlights, and expands nested nodes matching your query."
}
]
},
{
"date": "2026-02-18",
"changes": [

View File

@@ -1,29 +1,37 @@
import React, { useEffect, Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import Layout from './components/Layout';
import ErrorBoundary from './components/ErrorBoundary';
import Loading from './components/Loading';
import { initGA } from './utils/analytics';
import React, { useEffect, Suspense, lazy } from "react";
import {
BrowserRouter as Router,
Routes,
Route,
Navigate,
} from "react-router-dom";
import { HelmetProvider } from "react-helmet-async";
import Layout from "./components/Layout";
import ErrorBoundary from "./components/ErrorBoundary";
import Loading from "./components/Loading";
import { initGA } from "./utils/analytics";
import './index.css';
import "./index.css";
const Home = lazy(() => import('./pages/Home'));
const UrlTool = lazy(() => import('./pages/UrlTool'));
const Base64Tool = lazy(() => import('./pages/Base64Tool'));
const BeautifierTool = lazy(() => import('./pages/BeautifierTool'));
const DiffTool = lazy(() => import('./pages/DiffTool'));
const TextLengthTool = lazy(() => import('./pages/TextLengthTool'));
const ObjectEditor = lazy(() => import('./pages/ObjectEditor'));
const TableEditor = lazy(() => import('./pages/TableEditor'));
const InvoiceEditor = lazy(() => import('./pages/InvoiceEditor'));
const MarkdownEditor = lazy(() => import('./pages/MarkdownEditor'));
const InvoicePreview = lazy(() => import('./pages/InvoicePreview'));
const InvoicePreviewMinimal = lazy(() => import('./pages/InvoicePreviewMinimal'));
const ReleaseNotes = lazy(() => import('./pages/ReleaseNotes'));
const TermsOfService = lazy(() => import('./pages/TermsOfService'));
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy'));
const NotFound = lazy(() => import('./pages/NotFound'));
const Home = lazy(() => import("./pages/Home"));
const UrlTool = lazy(() => import("./pages/UrlTool"));
const Base64Tool = lazy(() => import("./pages/Base64Tool"));
const BeautifierTool = lazy(() => import("./pages/BeautifierTool"));
const DiffTool = lazy(() => import("./pages/DiffTool"));
const TextLengthTool = lazy(() => import("./pages/TextLengthTool"));
const ObjectEditor = lazy(() => import("./pages/ObjectEditor"));
const TableEditor = lazy(() => import("./pages/TableEditor"));
const InvoiceEditor = lazy(() => import("./pages/InvoiceEditor"));
const MarkdownEditor = lazy(() => import("./pages/MarkdownEditor"));
const DiagramEditor = lazy(() => import("./pages/DiagramEditor"));
const InvoicePreview = lazy(() => import("./pages/InvoicePreview"));
const InvoicePreviewMinimal = lazy(
() => import("./pages/InvoicePreviewMinimal"),
);
const ReleaseNotes = lazy(() => import("./pages/ReleaseNotes"));
const TermsOfService = lazy(() => import("./pages/TermsOfService"));
const PrivacyPolicy = lazy(() => import("./pages/PrivacyPolicy"));
const NotFound = lazy(() => import("./pages/NotFound"));
function App() {
// Initialize Google Analytics on app startup
@@ -48,9 +56,16 @@ function App() {
<Route path="/table-editor" element={<TableEditor />} />
<Route path="/invoice-editor" element={<InvoiceEditor />} />
<Route path="/markdown-editor" element={<MarkdownEditor />} />
<Route path="/diagram-editor" element={<DiagramEditor />} />
<Route path="/invoice-preview" element={<InvoicePreview />} />
<Route path="/invoice-preview-minimal" element={<InvoicePreviewMinimal />} />
<Route path="/whats-new" element={<Navigate to="/release-notes" replace />} />
<Route
path="/invoice-preview-minimal"
element={<InvoicePreviewMinimal />}
/>
<Route
path="/whats-new"
element={<Navigate to="/release-notes" replace />}
/>
<Route path="/release-notes" element={<ReleaseNotes />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} />

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,7 +3,7 @@ import React, { useEffect, useRef } from "react";
const AdBlock = ({
className = "",
adKey = "e0ca7c61c83457f093bbc2e261b43d31",
adDomain = "www.highperformanceformat.com",
adDomain = "downconvenientmagnetic.com",
}) => {
const iframeRef = useRef(null);

View File

@@ -0,0 +1,34 @@
import React from "react";
import { NodeViewWrapper, NodeViewContent } from "@tiptap/react";
const CodeBlockComponent = ({ node, updateAttributes, extension }) => {
return (
<NodeViewWrapper className="code-block-wrapper relative bg-[#0d1117] rounded-md overflow-hidden mb-[0.65em]">
<div className="code-block-header flex justify-between items-center px-4 py-2 bg-[#161b22] border border-[#30363d] border-b-0 rounded-t-md text-xs font-mono">
<select
contentEditable={false}
value={node.attrs.language || "text"}
onChange={(event) =>
updateAttributes({ language: event.target.value })
}
className="bg-transparent text-[#8b949e] border-none outline-none focus:ring-0 uppercase tracking-wider cursor-pointer"
>
<option value="text">text</option>
<option value="javascript">javascript</option>
<option value="typescript">typescript</option>
<option value="html">html</option>
<option value="css">css</option>
<option value="json">json</option>
<option value="bash">bash</option>
<option value="python">python</option>
<option value="sql">sql</option>
</select>
</div>
<pre className="!mt-0 !rounded-t-none !bg-transparent !border-[#30363d] !p-4 !text-[#e6edf3]">
<NodeViewContent as="code" />
</pre>
</NodeViewWrapper>
);
};
export default CodeBlockComponent;

View File

@@ -0,0 +1,107 @@
import React, { useEffect, useRef } from "react";
const FullscreenAdBanner = () => {
const desktopIframeRef = useRef(null);
const mobileIframeRef = useRef(null);
useEffect(() => {
// Initialize Desktop/Tablet Ad (728x90)
if (desktopIframeRef.current) {
const iframe = desktopIframeRef.current;
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; display: block; background: transparent; overflow: hidden; height: 100vh; }
</style>
</head>
<body>
<script type="text/javascript">
atOptions = {
'key' : '5d1186bf7f51a6e8732651b00fefc51b',
'format' : 'iframe',
'height' : 90,
'width' : 728,
'params' : {}
};
</script>
<script type="text/javascript" src="https://downconvenientmagnetic.com/5d1186bf7f51a6e8732651b00fefc51b/invoke.js"></script>
</body>
</html>
`);
doc.close();
}
// Initialize Mobile Ad (320x50) using the existing mobile key
if (mobileIframeRef.current) {
const iframe = mobileIframeRef.current;
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; padding: 0; display: block; background: transparent; overflow: hidden; height: 100vh; }
</style>
</head>
<body>
<script type="text/javascript">
atOptions = {
'key' : '2965bcf877388cafa84160592c550f5a',
'format' : 'iframe',
'height' : 50,
'width' : 320,
'params' : {}
};
</script>
<script type="text/javascript" src="https://downconvenientmagnetic.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
</body>
</html>
`);
doc.close();
}
}, []);
return (
<>
{/* Desktop & Tablet View (>= 768px) */}
<div className="absolute bottom-0 left-0 right-0 z-50 justify-center bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 py-2 shadow-lg hidden md:flex">
<iframe
ref={desktopIframeRef}
style={{
width: "728px",
height: "90px",
border: "none",
maxWidth: "100%",
}}
title="Fullscreen Advertisement Desktop"
sandbox="allow-scripts allow-same-origin"
/>
</div>
{/* Mobile View (< 768px) */}
<div className="absolute bottom-0 left-0 right-0 z-50 flex justify-center items-end bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 shadow-lg md:hidden h-[51px]">
<iframe
ref={mobileIframeRef}
style={{
width: "320px",
height: "50px",
border: "none",
maxWidth: "100%",
display: "block",
}}
title="Fullscreen Advertisement Mobile"
sandbox="allow-scripts allow-same-origin"
/>
</div>
</>
);
};
export default FullscreenAdBanner;

View File

@@ -1,18 +1,30 @@
import React, { useState, useEffect, useRef } from 'react';
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';
import React, { useState, useEffect, useRef } from "react";
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 {
showModal,
pendingNavigation,
handleConfirm,
handleCancel,
hasUnsavedData,
navigateWithGuard,
} = useNavigationGuard();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const dropdownRef = useRef(null);
@@ -32,9 +44,9 @@ const Layout = ({ children }) => {
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
@@ -45,10 +57,10 @@ const Layout = ({ children }) => {
}, [location.pathname]);
// Check if we're on a tool page (not homepage)
const isToolPage = location.pathname !== '/';
const isToolPage = location.pathname !== "/";
// Check if we're on invoice preview page (no sidebar needed)
const isInvoicePreviewPage = location.pathname === '/invoice-preview';
const isInvoicePreviewPage = location.pathname === "/invoice-preview";
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 flex flex-col">
@@ -57,9 +69,18 @@ const Layout = ({ children }) => {
{/* Header */}
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
<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={
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">
<button onClick={() => navigateWithGuard('/')} className="flex items-center group">
<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">
@@ -67,11 +88,11 @@ const Layout = ({ children }) => {
src="/logo.svg"
alt={SITE_CONFIG.title}
className="h-8 w-auto"
style={{ maxWidth: '150px' }}
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';
e.target.style.display = "none";
e.target.nextSibling.style.display = "flex";
}}
/>
<div className="hidden items-center space-x-3">
@@ -91,12 +112,12 @@ const Layout = ({ children }) => {
<button
onClick={() => {
setIsDropdownOpen(false);
navigateWithGuard('/');
navigateWithGuard("/");
}}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 ${
isActive('/')
? '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'
isActive("/")
? "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" />
@@ -113,9 +134,11 @@ const Layout = ({ children }) => {
>
<Sparkles className="h-4 w-4" />
<span>Tools</span>
<ChevronDown className={`h-4 w-4 transition-transform duration-300 ${
isDropdownOpen ? 'rotate-180' : ''
}`} />
<ChevronDown
className={`h-4 w-4 transition-transform duration-300 ${
isDropdownOpen ? "rotate-180" : ""
}`}
/>
</button>
{/* Dropdown Menu */}
@@ -125,7 +148,9 @@ const Layout = ({ children }) => {
<div className="relative">
{TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
const categoryConfig = getCategoryConfig(
tool.category,
);
return (
<button
@@ -136,16 +161,20 @@ const Layout = ({ children }) => {
}}
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'
? "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`}>
<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-600 dark:text-slate-600">{tool.description}</div>
<div className="text-xs text-slate-600 dark:text-slate-600">
{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-600" />
@@ -169,7 +198,11 @@ const Layout = ({ children }) => {
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
aria-expanded={isMobileMenuOpen}
>
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
{isMobileMenuOpen ? (
<X className="h-6 w-6" />
) : (
<Menu className="h-6 w-6" />
)}
</button>
</div>
</div>
@@ -202,12 +235,16 @@ const Layout = ({ children }) => {
}}
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'
? "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
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>
@@ -217,7 +254,7 @@ const Layout = ({ children }) => {
<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-600 dark:text-slate-600 uppercase tracking-wider px-4 py-2 flex items-center gap-2">
<Sparkles className="h-3 w-3" />
{isToolPage ? 'Switch Tools' : 'Tools'}
{isToolPage ? "Switch Tools" : "Tools"}
</div>
{TOOLS.map((tool) => {
const IconComponent = tool.icon;
@@ -232,16 +269,20 @@ const Layout = ({ children }) => {
}}
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-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'
? "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"
}`}
>
<div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm`}>
<div
className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm`}
>
<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-600 dark:text-slate-600">{tool.description}</div>
<div className="text-xs text-slate-600 dark:text-slate-600">
{tool.description}
</div>
</div>
</button>
);
@@ -254,7 +295,7 @@ const Layout = ({ children }) => {
)}
{/* Main Content */}
<div className="flex flex-1 pt-16 min-w-0 w-full max-w-full overflow-x-hidden">
<div className="flex flex-1 pt-16 min-w-0 w-full max-w-full">
{/* Main Content Area */}
<main className="flex-1 flex flex-col min-w-0 w-full max-w-full">
{isToolPage && !isInvoicePreviewPage ? (
@@ -263,22 +304,18 @@ const Layout = ({ children }) => {
<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">
<div className="p-4 sm:p-6 w-full min-w-0 max-w-full">
{children}
</div>
</div>
</div>
) : isInvoicePreviewPage ? (
<div className="flex-1 flex flex-col">
<div className="flex-1">
{children}
</div>
<div className="flex-1">{children}</div>
</div>
) : (
<div className="flex-1 flex flex-col">
<div className="flex-1">
{children}
</div>
<div className="flex-1">{children}</div>
{/* 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">
@@ -291,11 +328,11 @@ const Layout = ({ children }) => {
src="/icon-192x192.png"
alt={SITE_CONFIG.title}
className="h-16 w-auto"
style={{ maxWidth: '100px' }}
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';
e.target.style.display = "none";
e.target.nextSibling.style.display = "flex";
}}
/>
<div className="hidden items-center gap-3">
@@ -334,21 +371,25 @@ const Layout = ({ children }) => {
</div>
<div className="flex items-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
onClick={() => navigateWithGuard("/release-notes")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Release Notes
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<span className="text-slate-300 dark:text-slate-600">
</span>
<button
onClick={() => navigateWithGuard('/privacy')}
onClick={() => navigateWithGuard("/privacy")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<span className="text-slate-300 dark:text-slate-600">
</span>
<button
onClick={() => navigateWithGuard('/terms')}
onClick={() => navigateWithGuard("/terms")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
@@ -373,11 +414,11 @@ const Layout = ({ children }) => {
src="/icon-192x192.png"
alt={SITE_CONFIG.title}
className="h-16 w-auto"
style={{ maxWidth: '100px' }}
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';
e.target.style.display = "none";
e.target.nextSibling.style.display = "flex";
}}
/>
</div>
@@ -390,21 +431,21 @@ const Layout = ({ children }) => {
</div>
<div className="flex items-center justify-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
onClick={() => navigateWithGuard("/release-notes")}
className="text-slate-600 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')}
onClick={() => navigateWithGuard("/privacy")}
className="text-slate-600 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')}
onClick={() => navigateWithGuard("/terms")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service

View File

@@ -1,21 +1,10 @@
import React, { useEffect, useRef, useState } from "react";
import { X } from "lucide-react";
import React, { useEffect, useRef } from "react";
const MobileAdBanner = () => {
const [visible, setVisible] = useState(true);
const [closed, setClosed] = useState(false);
const iframeRef = useRef(null);
useEffect(() => {
const wasClosed = sessionStorage.getItem("mobileAdClosed");
if (wasClosed === "true") {
setClosed(true);
setVisible(false);
}
}, []);
useEffect(() => {
if (!visible || closed || !iframeRef.current) return;
if (!iframeRef.current) return;
const timer = setTimeout(() => {
if (!iframeRef.current) return;
@@ -29,7 +18,7 @@ const MobileAdBanner = () => {
<html>
<head>
<style>
body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; }
body { margin: 0; padding: 0; display: block; background: transparent; overflow: hidden; height: 100vh; }
</style>
</head>
<body>
@@ -42,7 +31,7 @@ const MobileAdBanner = () => {
'params' : {}
};
</script>
<script type="text/javascript" src="https://www.highperformanceformat.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
<script type="text/javascript" src="https://downconvenientmagnetic.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
</body>
</html>
`);
@@ -50,34 +39,22 @@ const MobileAdBanner = () => {
}, 500);
return () => clearTimeout(timer);
}, [visible, closed]);
const handleClose = () => {
setVisible(false);
setClosed(true);
sessionStorage.setItem("mobileAdClosed", "true");
};
if (!visible || closed) return null;
}, []);
return (
<div className="lg: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-2 right-2 p-1 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded-full shadow-sm z-10"
aria-label="Close ad"
>
<X className="h-4 w-4" />
</button>
<div className="flex justify-center items-center py-2">
<div className="lg: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 h-[51px] flex justify-center items-end">
<iframe
ref={iframeRef}
style={{ width: "320px", height: "50px", border: "none" }}
style={{
width: "320px",
height: "50px",
border: "none",
display: "block",
}}
title="Mobile Advertisement"
sandbox="allow-scripts allow-same-origin"
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,289 @@
import React, { useEffect } from "react";
import { useEditor, EditorContent, ReactNodeViewRenderer } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Link from "@tiptap/extension-link";
import Image from "@tiptap/extension-image";
import { Table } from "@tiptap/extension-table";
import { TableRow } from "@tiptap/extension-table-row";
import { TableHeader } from "@tiptap/extension-table-header";
import { TableCell } from "@tiptap/extension-table-cell";
import { TaskList } from "@tiptap/extension-task-list";
import { TaskItem } from "@tiptap/extension-task-item";
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import { common, createLowlight } from "lowlight";
import CodeBlockComponent from "./CodeBlockComponent";
import { Markdown } from "tiptap-markdown";
import {
Bold,
Italic,
Strikethrough,
Code,
Heading1,
Heading2,
Heading3,
List,
ListOrdered,
CheckSquare,
Quote,
Link2,
Image as ImageIcon,
Table as TableIcon,
Minus,
} from "lucide-react";
// Set up lowlight for syntax highlighting in Tiptap
const lowlight = createLowlight(common);
const MenuBar = ({ editor }) => {
if (!editor) return null;
return (
<div className="flex flex-wrap justify-center items-center gap-1 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10 w-full">
<div className="flex flex-wrap items-center gap-1 w-full max-w-[85ch]">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("bold") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Bold"
>
<Bold className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("italic") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Italic"
>
<Italic className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={!editor.can().chain().focus().toggleStrike().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("strike") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Strikethrough"
>
<Strikethrough className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={!editor.can().chain().focus().toggleCode().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("code") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Inline Code"
>
<Code className="h-4 w-4" />
</button>
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
<button
onClick={() =>
editor.chain().focus().toggleHeading({ level: 1 }).run()
}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("heading", { level: 1 }) ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Heading 1"
>
<Heading1 className="h-4 w-4" />
</button>
<button
onClick={() =>
editor.chain().focus().toggleHeading({ level: 2 }).run()
}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("heading", { level: 2 }) ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Heading 2"
>
<Heading2 className="h-4 w-4" />
</button>
<button
onClick={() =>
editor.chain().focus().toggleHeading({ level: 3 }).run()
}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("heading", { level: 3 }) ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Heading 3"
>
<Heading3 className="h-4 w-4" />
</button>
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("bulletList") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Bullet List"
>
<List className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("orderedList") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Ordered List"
>
<ListOrdered className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleTaskList().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("taskList") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Task List"
>
<CheckSquare className="h-4 w-4" />
</button>
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
<button
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("blockquote") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Blockquote"
>
<Quote className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("codeBlock") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Code Block"
>
<Code className="h-4 w-4" />
</button>
<button
onClick={() => editor.chain().focus().setHorizontalRule().run()}
className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-gray-600 dark:text-gray-300"
title="Horizontal Rule"
>
<Minus className="h-4 w-4" />
</button>
<div className="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" />
<button
onClick={() => {
const url = window.prompt("URL");
if (url) {
editor
.chain()
.focus()
.extendMarkRange("link")
.setLink({ href: url })
.run();
}
}}
className={`p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors ${editor.isActive("link") ? "bg-gray-200 dark:bg-gray-700 text-blue-600 dark:text-blue-400" : "text-gray-600 dark:text-gray-300"}`}
title="Add Link"
>
<Link2 className="h-4 w-4" />
</button>
<button
onClick={() => {
const url = window.prompt("Image URL");
if (url) {
editor.chain().focus().setImage({ src: url }).run();
}
}}
className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-gray-600 dark:text-gray-300"
title="Add Image"
>
<ImageIcon className="h-4 w-4" />
</button>
<button
onClick={() =>
editor
.chain()
.focus()
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run()
}
className="p-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-gray-600 dark:text-gray-300"
title="Insert Table"
>
<TableIcon className="h-4 w-4" />
</button>
</div>
</div>
);
};
const RichMarkdownEditor = ({
initialContent,
onChange,
className = "",
height = "600px",
isFullscreen = false,
}) => {
const editor = useEditor({
extensions: [
StarterKit.configure({
codeBlock: false, // We'll use our own codeblock extension
}),
CodeBlockLowlight.extend({
addNodeView() {
return ReactNodeViewRenderer(CodeBlockComponent);
},
}).configure({
lowlight,
}),
Link.configure({
openOnClick: false,
}),
Image,
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
TaskList,
TaskItem.configure({
nested: true,
}),
Markdown.configure({
html: true,
tightLists: true,
tightListClass: "tight",
bulletListMarker: "-",
linkify: true,
breaks: false,
}),
],
content: initialContent,
onUpdate: ({ editor }) => {
// Serialize back to markdown and send to parent
const markdownOutput = editor.storage.markdown.getMarkdown();
const htmlOutput = editor.getHTML();
onChange(markdownOutput, htmlOutput);
},
editorProps: {
attributes: {
class:
"prose prose-sm sm:prose dark:prose-invert prose-blue focus:outline-none w-full max-w-none",
},
},
});
// Update editor content when initialContent prop completely changes from outside (e.g. loading a template)
useEffect(() => {
if (editor && initialContent !== undefined) {
const currentMarkdown = editor.storage.markdown.getMarkdown();
if (initialContent !== currentMarkdown) {
editor.commands.setContent(initialContent);
}
}
}, [editor, initialContent]);
return (
<div
className={`flex flex-col bg-white dark:bg-gray-900 overflow-hidden ${className}`}
>
<MenuBar editor={editor} />
<div
className={`overflow-y-auto w-full custom-scrollbar flex justify-center p-6`}
style={{ height }}
>
<div
className={`w-full max-w-[85ch] markdown-content-wrapper ${isFullscreen ? "is-fullscreen" : "is-normal"} is-edit-mode`}
>
<EditorContent editor={editor} />
</div>
</div>
</div>
);
};
export default RichMarkdownEditor;

View File

@@ -1,19 +1,45 @@
import React, { useState, useEffect, useRef } from 'react';
import { Plus, Minus, ChevronDown, ChevronRight, Type, Hash, ToggleLeft, List, Braces, Edit3, X, Eye, Pencil } from 'lucide-react';
import React, { useState, useEffect, useRef } from "react";
import {
Plus,
Minus,
ChevronDown,
ChevronRight,
ChevronsUpDown,
ChevronsDownUp,
Type,
Hash,
ToggleLeft,
List,
Braces,
Edit3,
X,
Eye,
Pencil,
Search,
} from "lucide-react";
const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyProp = false }) => {
const StructuredEditor = ({
onDataChange,
initialData = {},
readOnly: readOnlyProp = false,
}) => {
const [data, setData] = useState(initialData);
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
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);
// Start in preview mode if readOnly is false
const [editMode, setEditMode] = useState(
readOnlyProp === false ? false : !readOnlyProp,
);
// Use internal editMode if readOnlyProp is not explicitly set, otherwise use prop
const readOnly = readOnlyProp !== false ? readOnlyProp : !editMode;
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState(new Set());
// Update internal data when initialData prop changes (but not from internal updates)
useEffect(() => {
// Skip update if this change came from internal editor actions
@@ -25,7 +51,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
setData(initialData);
// Expand root node if there's data
if (Object.keys(initialData).length > 0) {
setExpandedNodes(new Set(['root']));
setExpandedNodes(new Set(["root"]));
}
}, [initialData]);
@@ -37,13 +63,13 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
// 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') {
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, '\\"');
if (typeof data === "string") {
const escapedData = data.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
const byteLength = new TextEncoder().encode(escapedData).length;
return `s:${byteLength}:"${escapedData}";`;
}
@@ -52,72 +78,78 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
data.forEach((item, index) => {
result += phpSerialize(index) + phpSerialize(item);
});
result += '}';
result += "}";
return result;
}
if (typeof data === 'object') {
if (typeof data === "object") {
const keys = Object.keys(data);
let result = `a:${keys.length}:{`;
keys.forEach(key => {
keys.forEach((key) => {
result += phpSerialize(key) + phpSerialize(data[key]);
});
result += '}';
result += "}";
return result;
}
return 'N;';
return "N;";
};
const phpUnserialize = (str) => {
let index = 0;
const parseValue = () => {
if (index >= str.length) throw new Error('Unexpected end of string');
if (index >= str.length) throw new Error("Unexpected end of string");
const type = str[index];
if (type === 'N') {
if (type === "N") {
index += 2;
return null;
}
if (str[index + 1] !== ':') throw new Error(`Expected ':' after type '${type}'`);
if (str[index + 1] !== ":")
throw new Error(`Expected ':' after type '${type}'`);
index += 2;
switch (type) {
case 'b':
const boolVal = str[index] === '1';
case "b":
const boolVal = str[index] === "1";
index += 2;
return boolVal;
case 'i':
let intStr = '';
while (index < str.length && str[index] !== ';') intStr += str[index++];
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++];
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++];
case "s":
let lenStr = "";
while (index < str.length && str[index] !== ":")
lenStr += str[index++];
index++;
if (str[index] !== '"') throw new Error('Expected opening quote');
if (str[index] !== '"') throw new Error("Expected opening quote");
index++;
const byteLength = parseInt(lenStr);
if (byteLength === 0) {
index += 2;
return '';
return "";
}
let endQuotePos = -1;
for (let i = index; i < str.length - 1; i++) {
if (str[i] === '"' && str[i + 1] === ';') {
if (str[i] === '"' && str[i + 1] === ";") {
endQuotePos = i;
break;
}
}
if (endQuotePos === -1) throw new Error('Could not find closing quote');
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++];
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 = {};
@@ -139,13 +171,13 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
// Detect if a string contains JSON or serialized data
const detectNestedData = (value) => {
if (typeof value !== 'string' || value.length < 5) return null;
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 };
if (typeof parsed === "object" && parsed !== null) {
return { type: "json", data: parsed };
}
} catch (e) {
// Not JSON, continue
@@ -156,8 +188,8 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
// 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 };
if (typeof parsed === "object" && parsed !== null) {
return { type: "serialized", data: parsed };
}
}
} catch (e) {
@@ -182,9 +214,9 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
// Convert back to string based on type
let stringValue;
if (nestedEditModal.type === 'json') {
if (nestedEditModal.type === "json") {
stringValue = JSON.stringify(nestedData);
} else if (nestedEditModal.type === 'serialized') {
} else if (nestedEditModal.type === "serialized") {
stringValue = phpSerialize(nestedData);
}
@@ -212,8 +244,112 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
setExpandedNodes(newExpanded);
};
const expandAll = () => {
const allPaths = new Set(["root"]);
// Helper to traverse and collect all paths
const traverse = (obj, currentPath) => {
if (typeof obj === "object" && obj !== null) {
if (Array.isArray(obj)) {
obj.forEach((item, index) => {
const path = `${currentPath}.${index}`;
if (typeof item === "object" && item !== null) {
allPaths.add(path);
traverse(item, path);
}
});
} else {
Object.entries(obj).forEach(([key, value]) => {
const path = `${currentPath}.${key}`;
if (typeof value === "object" && value !== null) {
allPaths.add(path);
traverse(value, path);
}
});
}
}
};
traverse(data, "root");
setExpandedNodes(allPaths);
};
const collapseAll = () => {
setExpandedNodes(new Set(["root"]));
};
// Search effect to auto-expand paths containing matches
useEffect(() => {
if (!searchQuery.trim()) {
setSearchResults(new Set());
return;
}
const query = searchQuery.toLowerCase();
const results = new Set();
const pathsToExpand = new Set(["root"]);
// Returns true if a match is found in this node or its descendants
const searchTraverse = (obj, currentPath) => {
let foundInCurrent = false;
if (typeof obj === "object" && obj !== null) {
if (Array.isArray(obj)) {
obj.forEach((item, index) => {
const path = `${currentPath}.${index}`;
const keyMatches = index.toString().includes(query);
let foundInChild = false;
if (typeof item === "object" && item !== null) {
foundInChild = searchTraverse(item, path);
} else {
const valueStr = getDisplayValue(item).toLowerCase();
if (valueStr.includes(query)) foundInChild = true;
}
if (keyMatches || foundInChild) {
results.add(path);
pathsToExpand.add(currentPath);
pathsToExpand.add(path);
foundInCurrent = true;
}
});
} else {
Object.entries(obj).forEach(([key, value]) => {
const path = `${currentPath}.${key}`;
const keyMatches = key.toLowerCase().includes(query);
let foundInChild = false;
if (typeof value === "object" && value !== null) {
foundInChild = searchTraverse(value, path);
} else {
const valueStr = getDisplayValue(value).toLowerCase();
if (valueStr.includes(query)) foundInChild = true;
}
if (keyMatches || foundInChild) {
results.add(path);
pathsToExpand.add(currentPath);
pathsToExpand.add(path);
foundInCurrent = true;
}
});
}
}
return foundInCurrent;
};
searchTraverse(data, "root");
setSearchResults(results);
// Merge expanded nodes with paths that need to be expanded for search
if (results.size > 0) {
setExpandedNodes((prev) => new Set([...prev, ...pathsToExpand]));
}
}, [searchQuery, data]);
const addProperty = (obj, path) => {
const pathParts = path.split('.');
const pathParts = path.split(".");
const newData = { ...data };
let current = newData;
@@ -225,15 +361,15 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
// Add new property to the target object
const keys = Object.keys(current);
const newKey = `property${keys.length + 1}`;
current[newKey] = '';
current[newKey] = "";
updateData(newData);
setExpandedNodes(new Set([...expandedNodes, path]));
};
const addArrayItem = (arr, path) => {
const newArr = [...arr, ''];
const pathParts = path.split('.');
const newArr = [...arr, ""];
const pathParts = path.split(".");
const newData = { ...data };
let current = newData;
@@ -251,7 +387,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
};
const removeProperty = (key, parentPath) => {
const pathParts = parentPath.split('.');
const pathParts = parentPath.split(".");
const newData = { ...data };
let current = newData;
@@ -261,7 +397,8 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
}
// Remove field type tracking for the removed property
const removedPath = parentPath === 'root' ? `root.${key}` : `${parentPath}.${key}`;
const removedPath =
parentPath === "root" ? `root.${key}` : `${parentPath}.${key}`;
const newFieldTypes = { ...fieldTypes };
delete newFieldTypes[removedPath];
setFieldTypes(newFieldTypes);
@@ -276,16 +413,16 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
}
// Check if we're removing from root level and it's the last property
if (parentPath === 'root' && Object.keys(newData).length === 0) {
if (parentPath === "root" && Object.keys(newData).length === 0) {
// Add an empty property to maintain initial state, like TableEditor maintains at least one row
newData[''] = '';
newData[""] = "";
}
updateData(newData);
};
const updateValue = (value, path) => {
const pathParts = path.split('.');
const pathParts = path.split(".");
const newData = { ...data };
let current = newData;
@@ -298,29 +435,30 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
const currentType = typeof currentValue;
// Preserve the current type when updating value
if (currentType === 'boolean') {
current[key] = value === 'true';
} else if (currentType === 'number') {
if (currentType === "boolean") {
current[key] = value === "true";
} else if (currentType === "number") {
const numValue = Number(value);
current[key] = isNaN(numValue) ? 0 : numValue;
} else if (currentValue === null) {
current[key] = value === 'null' ? null : value;
current[key] = value === "null" ? null : value;
} else {
// For strings and initial empty values, use smart detection
if (currentValue === '' || currentValue === undefined) {
if (currentValue === "" || currentValue === undefined) {
// Check if this is a newly added property (starts with "property" + number)
const isNewProperty = typeof key === 'string' && key.match(/^property\d+$/);
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') {
if (value === "true" || value === "false") {
current[key] = value === "true";
} else if (value === "null") {
current[key] = null;
} else if (!isNaN(value) && value !== '' && value.trim() !== '') {
} else if (!isNaN(value) && value !== "" && value.trim() !== "") {
current[key] = Number(value);
} else {
current[key] = value;
@@ -336,7 +474,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
};
const changeType = (newType, path) => {
const pathParts = path.split('.');
const pathParts = path.split(".");
const newData = { ...data };
let current = newData;
@@ -354,69 +492,98 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
// Try to preserve value when changing types if possible
switch (newType) {
case 'string':
case 'longtext':
current[key] = currentValue === null ? '' : currentValue.toString();
case "string":
case "longtext":
current[key] = currentValue === null ? "" : currentValue.toString();
break;
case 'number':
if (typeof currentValue === 'string' && !isNaN(currentValue) && currentValue.trim() !== '') {
case "number":
if (
typeof currentValue === "string" &&
!isNaN(currentValue) &&
currentValue.trim() !== ""
) {
current[key] = Number(currentValue);
} else if (typeof currentValue === 'boolean') {
} else if (typeof currentValue === "boolean") {
current[key] = currentValue ? 1 : 0;
} else {
current[key] = 0;
}
break;
case 'boolean':
if (typeof currentValue === 'string') {
current[key] = currentValue.toLowerCase() === 'true';
} else if (typeof currentValue === 'number') {
case "boolean":
if (typeof currentValue === "string") {
current[key] = currentValue.toLowerCase() === "true";
} else if (typeof currentValue === "number") {
current[key] = currentValue !== 0;
} else {
current[key] = false;
}
break;
case 'array':
case "array":
current[key] = [];
break;
case 'object':
case "object":
current[key] = {};
break;
case 'null':
case "null":
current[key] = null;
break;
default:
current[key] = '';
current[key] = "";
}
updateData(newData);
setExpandedNodes(new Set([...expandedNodes, path]));
};
// Helper function to display string values with proper unescaping
const getDisplayValue = (value) => {
if (value === null) return 'null';
if (value === undefined) return '';
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') {
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)
.replace(/\\\//g, "/") // Unescape forward slashes
.replace(/\\\\/g, "\\"); // Unescape backslashes (do this last)
}
return stringValue;
};
// Helper function to render text with search highlighting
const renderHighlightedText = (text) => {
if (!searchQuery.trim() || typeof text !== "string") return text;
const query = searchQuery.toLowerCase();
const textLower = text.toLowerCase();
const index = textLower.indexOf(query);
if (index === -1) return text;
const before = text.substring(0, index);
const match = text.substring(index, index + query.length);
const after = text.substring(index + query.length);
return (
<>
{before}
<span className="bg-yellow-200 dark:bg-yellow-800 text-black dark:text-white rounded-sm">
{match}
</span>
{renderHighlightedText(after)}{" "}
{/* Handle multiple matches in same string if needed */}
</>
);
};
const renameKey = (oldKey, newKey, path) => {
if (oldKey === newKey || !newKey.trim()) return;
const pathParts = path.split('.');
const pathParts = path.split(".");
const newData = { ...data };
let current = newData;
@@ -465,7 +632,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
);
}
if (typeof value === 'string') {
if (typeof value === "string") {
return (
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300">
<Type className="h-3 w-3" />
@@ -473,7 +640,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
);
}
if (typeof value === 'number') {
if (typeof value === "number") {
return (
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-300">
<Hash className="h-3 w-3" />
@@ -481,7 +648,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
);
}
if (typeof value === 'boolean') {
if (typeof value === "boolean") {
return (
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300">
<ToggleLeft className="h-3 w-3" />
@@ -497,7 +664,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
);
}
if (typeof value === 'object') {
if (typeof value === "object") {
return (
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300">
<Braces className="h-3 w-3" />
@@ -514,16 +681,41 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
const renderValue = (value, key, path, parentPath) => {
const isExpanded = expandedNodes.has(path);
const canExpand = typeof value === 'object' && value !== null;
const canExpand = typeof value === "object" && value !== null;
// Check if this node matches the search query (if active)
// A node matches if its path is in searchResults
const isSearchActive = searchQuery.trim() !== "";
const isMatch = isSearchActive && searchResults.has(path);
// Check if any of its children match
let hasMatchingChildren = false;
if (isSearchActive && canExpand) {
// Look through searchResults to see if any path starts with this node's path
hasMatchingChildren = Array.from(searchResults).some((resPath) =>
resPath.startsWith(`${path}.`),
);
}
// Hide node if:
// 1. Search is active AND
// 2. Node itself doesn't match AND
// 3. Node has no matching children AND
// 4. Node is not the root (we always render root level if it matches something to keep structure)
// Exception: If we're at root level but the node has no matches and no matching children, hide it
const isHiddenBySearch = isSearchActive && !isMatch && !hasMatchingChildren;
// If there is an active search and this node doesn't match and has no matching children, don't render it
if (isHiddenBySearch) return null;
// Check if parent is an array by looking at the parent path
const isArrayItem = (() => {
if (parentPath === 'root') {
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('.');
const parentPathParts = parentPath.split(".");
let current = data;
for (let i = 1; i < parentPathParts.length; i++) {
current = current[parentPathParts[i]];
@@ -533,7 +725,10 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
})();
return (
<div key={path} className="ml-4 border-l border-gray-200 dark:border-gray-700 pl-4 overflow-hidden">
<div
key={path}
className="ml-4 border-l border-gray-200 dark:border-gray-700 pl-4 overflow-hidden"
>
<div className="flex items-center space-x-2 mb-2">
{canExpand && (
<button
@@ -553,38 +748,41 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
<div className="flex items-center space-x-2 flex-1">
{isArrayItem ? (
// Array items: icon + index span (compact)
<>
<div className="flex items-center space-x-1 w-[120px] shrink-0">
{getTypeIcon(value)}
<span className="px-2 py-1 text-sm text-gray-600 dark:text-gray-600 font-mono whitespace-nowrap">
[{key}]
<span className="text-gray-500 dark:text-gray-400 font-mono text-sm">
{renderHighlightedText(key)}
</span>
</>
<span className="text-gray-600 inline">:</span>
</div>
) : (
// Object properties: icon + editable key + colon (compact)
// Object properties: icon + editable key input
<>
{getTypeIcon(value)}
{readOnly ? (
<span className="px-2 py-1 text-sm text-gray-900 dark:text-gray-100 font-mono">
{key}
<span
className="px-2 py-1 text-sm font-medium text-gray-900 dark:text-gray-100 min-w-0 break-all"
style={{ width: "120px" }}
>
{renderHighlightedText(key)}
</span>
) : (
<input
type="text"
defaultValue={key}
onBlur={(e) => {
const newKey = e.target.value.trim();
if (newKey && newKey !== key) {
renameKey(key, newKey, path);
if (e.target.value !== key) {
renameKey(key, e.target.value, path);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
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
style={{ width: "120px" }} // Fixed width for consistency
/>
)}
<span className="text-gray-600 inline">:</span>
@@ -592,23 +790,23 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
)}
{!canExpand ? (
typeof value === 'boolean' ? (
typeof value === "boolean" ? (
<div className="flex-1 flex items-center space-x-2">
{readOnly ? (
<span className="text-sm text-gray-900 dark:text-gray-100 font-mono">
{value.toString()}
{renderHighlightedText(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'
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'
value ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
@@ -621,22 +819,23 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
) : (
<div className="flex-1 flex items-center gap-2">
{readOnly ? (
typeof value === 'string' && detectNestedData(value) ? (
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)}
{renderHighlightedText(getDisplayValue(value))}
</span>
) : (
<span className="px-2 py-1 text-sm text-gray-900 dark:text-gray-100 font-mono break-all">
{getDisplayValue(value)}
{renderHighlightedText(getDisplayValue(value))}
</span>
)
) : (
<>
{(fieldTypes[path] === 'longtext' || (typeof value === 'string' && value.includes('\n'))) ? (
{fieldTypes[path] === "longtext" ||
(typeof value === "string" && value.includes("\n")) ? (
<textarea
value={getDisplayValue(value)}
onChange={(e) => updateValue(e.target.value, path)}
@@ -653,7 +852,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
placeholder="Value"
/>
)}
{typeof value === 'string' && detectNestedData(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"
@@ -668,7 +867,9 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
)
) : (
<span className="flex-1 text-sm text-gray-600 dark:text-gray-600">
{Array.isArray(value) ? `Array (${value.length} items)` : `Object (${Object.keys(value).length} properties)`}
{Array.isArray(value)
? `Array (${value.length} items)`
: `Object (${Object.keys(value).length} properties)`}
</span>
)}
@@ -676,14 +877,22 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
<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'
)
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"
@@ -714,7 +923,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
{Array.isArray(value) ? (
<>
{value.map((item, index) =>
renderValue(item, index.toString(), `${path}.${index}`, path)
renderValue(item, index.toString(), `${path}.${index}`, path),
)}
{!readOnly && (
<button
@@ -729,7 +938,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
) : (
<>
{Object.entries(value).map(([k, v]) =>
renderValue(v, k, `${path}.${k}`, path)
renderValue(v, k, `${path}.${k}`, path),
)}
{!readOnly && (
<button
@@ -751,18 +960,63 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
return (
<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>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-3">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 whitespace-nowrap">
Structured Data Editor
</h3>
<div className="flex flex-wrap items-center gap-3 w-full sm:w-auto sm:justify-end">
{/* Search Bar Inline */}
<div className="relative flex-grow max-w-[200px] sm:max-w-xs order-last sm:order-first">
<div className="absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none">
<Search className="h-3.5 w-3.5 text-gray-400" />
</div>
<input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-8 pr-8 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-xs text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="absolute inset-y-0 right-0 pr-2.5 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
>
<X className="h-3.5 w-3.5" />
</button>
)}
</div>
{/* Expand/Collapse All Buttons */}
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm shrink-0">
<button
onClick={expandAll}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium 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 transition-colors"
title="Expand All"
>
<ChevronsUpDown className="h-3.5 w-3.5" />
<span className="hidden lg:inline">Expand All</span>
</button>
<button
onClick={collapseAll}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium 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 transition-colors border-l border-gray-200 dark:border-gray-700"
title="Collapse All"
>
<ChevronsDownUp className="h-3.5 w-3.5" />
<span className="hidden lg:inline">Collapse All</span>
</button>
</div>
{/* 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">
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm shrink-0">
<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-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
? "bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300"
: "text-gray-600 dark:text-gray-600 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" />
@@ -772,8 +1026,8 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
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-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
? "bg-orange-50 dark:bg-orange-900/20 text-orange-700 dark:text-orange-300"
: "text-gray-600 dark:text-gray-600 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" />
@@ -783,6 +1037,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
)}
</div>
</div>
</div>
<div className="w-full overflow-hidden">
<div className="w-full overflow-x-auto">
@@ -790,18 +1045,21 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
{Object.keys(data).length === 0 ? (
<div className="text-center text-gray-600 dark:text-gray-600 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>
<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')
renderValue(value, key, `root.${key}`, "root"),
)
)}
{/* Root level Add Property button */}
{!readOnly && (
<button
onClick={() => addProperty(data, 'root')}
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" />
@@ -820,10 +1078,13 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
<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
Edit Nested{" "}
{nestedEditModal.type === "json" ? "JSON" : "Serialized"} Data
</h3>
<p className="text-sm text-gray-600 dark:text-gray-600 mt-1">
Changes will be saved back as a {nestedEditModal.type === 'json' ? 'JSON' : 'serialized'} string
Changes will be saved back as a{" "}
{nestedEditModal.type === "json" ? "JSON" : "serialized"}{" "}
string
</p>
</div>
<button

View File

@@ -13,7 +13,7 @@ const TabletAdSection = () => {
<div className="flex-shrink-0">
<AdBlock
adKey="7c55aebcdd74f6e9a8dc24bd13e7d949"
adDomain="www.highperformanceformat.com"
adDomain="downconvenientmagnetic.com"
/>
</div>
<div className="flex-shrink-0">

View File

@@ -1,163 +1,185 @@
import { Edit3, Table, LinkIcon, Hash, Wand2, GitCompare, Type, Home, FileText } from 'lucide-react';
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-600'
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-600",
},
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'
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'
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'
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'
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'
}
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',
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'
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',
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'
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',
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'
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',
path: "/diagram-editor",
name: "Diagram Editor",
icon: Edit3,
description:
"Create diagrams as code using Mermaid.js with live preview and multi-format export",
tags: ["Diagram", "Mermaid", "Architecture", "Flowchart", "Visual"],
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'
description:
"Create, edit, and export professional invoices with PDF generation",
tags: ["Invoice", "PDF", "Business", "Billing", "Export"],
category: "editor",
},
{
path: '/url',
name: 'URL Encoder/Decoder',
path: "/url",
name: "URL Encoder/Decoder",
icon: LinkIcon,
description: 'Encode and decode URLs and query parameters',
tags: ['URL', 'Encode', 'Decode'],
category: 'encoder'
description: "Encode and decode URLs and query parameters",
tags: ["URL", "Encode", "Decode"],
category: "encoder",
},
{
path: '/base64',
name: 'Base64 Encoder/Decoder',
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'
description: "Convert text to Base64 and back with support for files",
tags: ["Base64", "Encode", "Binary"],
category: "encoder",
},
{
path: '/beautifier',
name: 'Code Beautifier/Minifier',
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'
description: "Format and minify JSON, XML, SQL, CSS, and HTML code",
tags: ["Format", "Minify", "Beautify"],
category: "formatter",
},
{
path: '/diff',
name: 'Text Diff Checker',
path: "/diff",
name: "Text Diff Checker",
icon: GitCompare,
description: 'Compare two texts and highlight differences line by line',
tags: ['Diff', 'Compare', 'Text'],
category: 'analyzer'
description: "Compare two texts and highlight differences line by line",
tags: ["Diff", "Compare", "Text"],
category: "analyzer",
},
{
path: '/text-length',
name: 'Text Length Checker',
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'
}
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',
path: "/",
name: "Home",
icon: Home,
description: 'Back to homepage',
category: 'non_tools'
}
description: "Back to homepage",
category: "non_tools",
},
];
// Navigation tools (for sidebar) - combines non-tools and tools
export const NAVIGATION_TOOLS = [
...NON_TOOLS,
...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',
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
totalTools: TOOLS.length,
};
// Helper functions
export const getCategoryConfig = (categoryKey) => TOOL_CATEGORIES[categoryKey] || TOOL_CATEGORIES.navigation;
export const getCategoryConfig = (categoryKey) =>
TOOL_CATEGORIES[categoryKey] || TOOL_CATEGORIES.navigation;
export const getToolsByCategory = (categoryKey) => TOOLS.filter(tool => tool.category === categoryKey);
export const getToolsByCategory = (categoryKey) =>
TOOLS.filter((tool) => tool.category === categoryKey);
export const getCategoryStats = () => {
const stats = {};
Object.keys(TOOL_CATEGORIES).forEach(key => {
if (key !== 'navigation') {
Object.keys(TOOL_CATEGORIES).forEach((key) => {
if (key !== "navigation") {
stats[key] = getToolsByCategory(key).length;
}
});

View File

@@ -2,31 +2,37 @@
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap');
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap");
@layer base {
html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow-x: hidden;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
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 {
font-family: 'JetBrains Mono', Monaco, 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', monospace;
code,
pre {
font-family:
"JetBrains Mono", Monaco, "Cascadia Code", "Segoe UI Mono",
"Roboto Mono", monospace;
}
}

586
src/pages/DiagramEditor.js Executable file
View File

@@ -0,0 +1,586 @@
import React, { useState, useRef, useEffect } from "react";
import {
GitGraph,
Plus,
Upload,
Download,
Globe,
Type,
Columns,
Maximize2,
Minimize2,
FileImage,
FileCode2,
Eye,
AlertTriangle,
FileText,
Copy,
} from "lucide-react";
import ToolLayout from "../components/ToolLayout";
import CodeMirrorEditor from "../components/CodeMirrorEditor";
import SEO from "../components/SEO";
import RelatedTools from "../components/RelatedTools";
import ReactFlowEditor from "../components/diagram/ReactFlowEditor";
import FullscreenAdBanner from "../components/FullscreenAdBanner";
import { toPng } from "html-to-image";
import { generateMermaidFromGraph } from "../utils/mermaidGenerator";
const DIAGRAM_TEMPLATES = {
flowchart: {
nodes: [
{
id: "1",
type: "process",
position: { x: 250, y: 50 },
data: { label: "Start" },
},
{
id: "2",
type: "decision",
position: { x: 250, y: 150 },
data: { label: "Is it OK?" },
},
{
id: "3",
type: "process",
position: { x: 100, y: 250 },
data: { label: "Fix it" },
},
{
id: "4",
type: "database",
position: { x: 400, y: 250 },
data: { label: "Save to DB" },
},
{
id: "5",
type: "process",
position: { x: 250, y: 350 },
data: { label: "End" },
},
],
edges: [
{ id: "e1-2", source: "1", target: "2" },
{
id: "e2-3",
source: "2",
target: "3",
sourceHandle: "left",
label: "No",
},
{
id: "e2-4",
source: "2",
target: "4",
sourceHandle: "right",
label: "Yes",
},
{ id: "e3-2", source: "3", target: "2", targetHandle: "left" },
{
id: "e2-5",
source: "2",
target: "5",
sourceHandle: "bottom",
targetHandle: "top",
},
],
},
database: {
nodes: [
{
id: "users",
type: "database",
position: { x: 100, y: 100 },
data: { label: "Users Table" },
},
{
id: "orders",
type: "database",
position: { x: 400, y: 100 },
data: { label: "Orders Table" },
},
{
id: "process",
type: "process",
position: { x: 250, y: 250 },
data: { label: "Order Processor" },
},
],
edges: [
{ id: "eu-p", source: "users", target: "process", label: "Read" },
{ id: "ep-o", source: "process", target: "orders", label: "Write" },
],
},
};
const DiagramEditor = () => {
const [graphData, setGraphData] = useState(DIAGRAM_TEMPLATES.flowchart);
const [code, setCode] = useState(
JSON.stringify(DIAGRAM_TEMPLATES.flowchart, null, 2),
);
const [activeTab, setActiveTab] = useState("create");
const [viewMode, setViewMode] = useState(() =>
window.innerWidth < 1024 ? "editor" : "split",
);
const [isFullscreen, setIsFullscreen] = useState(false);
const [error, setError] = useState(null);
const [fetchUrl, setFetchUrl] = useState("");
const [fetching, setFetching] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState("flowchart");
const reactFlowWrapper = useRef(null);
const fileInputRef = useRef(null);
// Sync JSON text input to Graph data
useEffect(() => {
try {
const parsed = JSON.parse(code);
if (parsed.nodes && parsed.edges) {
setGraphData(parsed);
setError(null);
}
} catch (err) {
setError(err.message);
}
}, [code]);
const handleGraphChange = (newGraphData) => {
setGraphData(newGraphData);
setCode(JSON.stringify(newGraphData, null, 2));
};
// Input Handlers
const handleTemplateChange = (e) => {
const template = e.target.value;
setSelectedTemplate(template);
if (DIAGRAM_TEMPLATES[template]) {
setGraphData(DIAGRAM_TEMPLATES[template]);
setCode(JSON.stringify(DIAGRAM_TEMPLATES[template], null, 2));
}
};
const handleFileUpload = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
setCode(e.target.result);
setActiveTab("create");
};
reader.onerror = () => setError("Failed to read file");
reader.readAsText(file);
e.target.value = ""; // Reset input
};
const handleFetchFromURL = async () => {
if (!fetchUrl.trim()) return;
setFetching(true);
try {
let url = fetchUrl.trim();
if (
url.includes("github.com") &&
!url.includes("raw.githubusercontent.com")
) {
url = url
.replace("github.com", "raw.githubusercontent.com")
.replace("/blob/", "/");
}
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const text = await response.text();
setCode(text);
setActiveTab("create");
} catch (err) {
setError(`Fetch failed: ${err.message}`);
} finally {
setFetching(false);
}
};
// Export Handlers (Updated for ReactFlow)
const downloadFile = (content, filename, mimeType) => {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const exportJSON = () => {
downloadFile(code, "diagram.json", "application/json");
};
const exportMermaid = () => {
const mermaidCode = generateMermaidFromGraph(
graphData.nodes,
graphData.edges,
);
downloadFile(mermaidCode, "diagram.mmd", "text/plain");
};
const copyMermaid = () => {
const mermaidCode = generateMermaidFromGraph(
graphData.nodes,
graphData.edges,
);
navigator.clipboard
.writeText(mermaidCode)
.then(() => {
// Could add toast notification here
console.log("Mermaid code copied!");
})
.catch((err) => console.error("Failed to copy", err));
};
const exportPNG = () => {
const flowElement = document.querySelector(".react-flow");
if (!flowElement) return;
toPng(flowElement, {
backgroundColor: "#ffffff",
width: flowElement.offsetWidth,
height: flowElement.offsetHeight,
style: {
width: "100%",
height: "100%",
transform: "translate(0, 0)",
},
})
.then((dataUrl) => {
const a = document.createElement("a");
a.setAttribute("download", "diagram.png");
a.setAttribute("href", dataUrl);
a.click();
})
.catch((error) => {
setError("Failed to export PNG: " + error.message);
});
};
return (
<>
<SEO
title="Mermaid Diagram Editor & Viewer"
description="Write Mermaid.js diagram code with live visual preview. Pan, zoom, and export your flowcharts, sequence diagrams, and architecture maps to PNG or SVG."
keywords="mermaid editor, diagram editor, diagram as code, mermaidjs, flowchart generator, sequence diagram, architecture diagram, export svg"
path="/diagram-editor"
toolId="diagram-editor"
/>
<ToolLayout
title="Diagram Tool"
description="Create diagrams as code using Mermaid.js with live preview and multi-format export."
icon={GitGraph}
>
{/* Input Section - Always visible */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mb-6">
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Get Started
</h3>
</div>
{/* Tab Navigation */}
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
<button
onClick={() => setActiveTab("create")}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "create"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
}`}
>
<Plus className="h-4 w-4 flex-shrink-0" />
Create New
</button>
<button
onClick={() => setActiveTab("url")}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "url"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
}`}
>
<Globe className="h-4 w-4 flex-shrink-0" />
URL Fetch
</button>
<button
onClick={() => setActiveTab("paste")}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "paste"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
}`}
>
<FileText className="h-4 w-4 flex-shrink-0" />
Paste
</button>
<button
onClick={() => setActiveTab("open")}
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
activeTab === "open"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
}`}
>
<Upload className="h-4 w-4 flex-shrink-0" />
Open File
</button>
</div>
<div className="p-4">
{activeTab === "create" && (
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Load Boilerplate Template
</label>
<select
value={selectedTemplate}
onChange={handleTemplateChange}
className="tool-input w-full max-w-xs"
>
<option value="flowchart">Flowchart</option>
<option value="database">Database/ER</option>
</select>
</div>
<button
onClick={() => {
setGraphData({ nodes: [], edges: [] });
setCode(JSON.stringify({ nodes: [], edges: [] }, null, 2));
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Clear Editor
</button>
</div>
)}
{activeTab === "url" && (
<div className="space-y-3">
<div className="flex gap-2">
<input
type="url"
value={fetchUrl}
onChange={(e) => setFetchUrl(e.target.value)}
onKeyPress={(e) =>
e.key === "Enter" && !fetching && handleFetchFromURL()
}
placeholder="https://raw.githubusercontent.com/.../diagram.mmd"
className="tool-input flex-1"
/>
<button
onClick={handleFetchFromURL}
disabled={fetching || !fetchUrl.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors whitespace-nowrap"
>
{fetching ? "Fetching..." : "Fetch"}
</button>
</div>
</div>
)}
{activeTab === "paste" && (
<div className="space-y-3">
<div className="flex gap-2">
<textarea
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Paste your diagram JSON here..."
className="tool-input h-32 flex-1"
/>
</div>
</div>
)}
{activeTab === "open" && (
<div className="space-y-3">
<input
ref={fileInputRef}
type="file"
accept=".mmd,.mermaid,.txt"
onChange={handleFileUpload}
className="tool-input"
/>
</div>
)}
</div>
</div>
{/* Main Editor Section */}
<div
className={`bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden min-w-0 w-full max-w-full ${isFullscreen ? "fixed inset-0 z-[9999] flex flex-col !mt-0" : "mb-6"}`}
>
{/* Header & Controls */}
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700 sticky top-0 bg-white dark:bg-gray-800 z-10 flex flex-wrap justify-between items-center gap-3">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<GitGraph className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Diagram Editor
</h3>
<div className="flex items-center gap-2">
<div className="flex border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-white dark:bg-gray-800 shadow-sm">
<button
onClick={() => setViewMode("editor")}
className={`flex items-center gap-2 px-3 py-1.5 text-sm font-medium transition-colors ${viewMode === "editor" ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300" : "text-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"}`}
>
<Type className="h-4 w-4" />{" "}
<span className="hidden sm:inline">Code</span>
</button>
<button
onClick={() => setViewMode("split")}
className={`hidden lg:flex items-center gap-2 px-3 py-1.5 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${viewMode === "split" ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300" : "text-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"}`}
>
<Columns className="h-4 w-4" />{" "}
<span className="hidden sm:inline">Split</span>
</button>
<button
onClick={() => setViewMode("preview")}
className={`flex items-center gap-2 px-3 py-1.5 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${viewMode === "preview" ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300" : "text-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"}`}
>
<Eye className="h-4 w-4" />{" "}
<span className="hidden sm:inline">Preview</span>
</button>
</div>
<button
onClick={() => setIsFullscreen(!isFullscreen)}
className="p-2 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
title={isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
>
{isFullscreen ? (
<Minimize2 className="h-4 w-4" />
) : (
<Maximize2 className="h-4 w-4" />
)}
</button>
</div>
</div>
{/* Split View Content */}
<div
className={`${viewMode === "split" ? "grid grid-cols-1 lg:grid-cols-2" : ""} overflow-hidden min-w-0 w-full flex-1 relative`}
>
{isFullscreen && <FullscreenAdBanner />}
{/* Editor Pane */}
{(viewMode === "editor" || viewMode === "split") && (
<div
className={`${viewMode === "split" ? "border-r border-gray-200 dark:border-gray-700" : ""} ${isFullscreen ? "h-[calc(100vh-60px)] pb-[90px]" : "h-[600px]"} w-full min-w-0 flex flex-col relative`}
>
<CodeMirrorEditor
value={code}
onChange={setCode}
language="json"
placeholder="Write your diagram JSON here..."
showToggle={false}
maxLines={999}
height="100%"
className="flex-1 h-full"
/>
</div>
)}
{/* Preview Pane */}
{(viewMode === "preview" || viewMode === "split") && (
<div
className={`${isFullscreen ? "h-[calc(100vh-60px)] pb-[90px]" : "h-[600px]"} w-full min-w-0 bg-slate-50 dark:bg-slate-900 flex flex-col relative`}
>
{error && (
<div className="absolute inset-0 flex flex-col items-center justify-center p-6 text-center z-10 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm">
<div className="p-4 bg-red-50 dark:bg-red-900/20 rounded-full mb-3">
<AlertTriangle className="h-8 w-8 text-red-500" />
</div>
<h4 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
Syntax Error
</h4>
<p className="text-sm text-red-600 dark:text-red-400 max-w-md">
{error}
</p>
</div>
)}
<div
className="flex-1 relative overflow-hidden"
ref={reactFlowWrapper}
>
<ReactFlowEditor
initialNodes={graphData.nodes}
initialEdges={graphData.edges}
onGraphChange={handleGraphChange}
/>
</div>
</div>
)}
</div>
</div>
{/* Export Section */}
{code.trim() && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden mt-6">
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Download className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Export Diagram
</h3>
</div>
<div className="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
<button
onClick={exportPNG}
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all group"
>
<FileImage className="h-6 w-6 text-gray-500 group-hover:text-blue-500 mb-2" />
<span className="font-medium text-gray-900 dark:text-white">
PNG Image
</span>
<span className="text-xs text-gray-500">
High-res rendering
</span>
</button>
<button
onClick={exportJSON}
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-all group"
>
<FileCode2 className="h-6 w-6 text-gray-500 group-hover:text-purple-500 mb-2" />
<span className="font-medium text-gray-900 dark:text-white">
JSON Schema
</span>
<span className="text-xs text-gray-500">.json data format</span>
</button>
<button
onClick={exportMermaid}
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-green-500 hover:bg-green-50 dark:hover:bg-green-900/20 transition-all group"
>
<FileCode2 className="h-6 w-6 text-gray-500 group-hover:text-green-500 mb-2" />
<span className="font-medium text-gray-900 dark:text-white">
Mermaid File
</span>
<span className="text-xs text-gray-500">.mmd raw code</span>
</button>
<button
onClick={copyMermaid}
className="flex flex-col items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-orange-500 hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-all group"
>
<Copy className="h-6 w-6 text-gray-500 group-hover:text-orange-500 mb-2" />
<span className="font-medium text-gray-900 dark:text-white">
Copy Mermaid
</span>
<span className="text-xs text-gray-500">To clipboard</span>
</button>
</div>
</div>
)}
<RelatedTools toolId="diagram-editor" />
</ToolLayout>
</>
);
};
export default DiagramEditor;

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,9 @@
/* GitHub-style Markdown Preview Styling */
.markdown-preview {
color: #24292f;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica,
Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
word-wrap: break-word;
@@ -85,7 +87,9 @@
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;
font-family:
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
"Liberation Mono", monospace;
}
.dark .markdown-preview code {
@@ -378,3 +382,95 @@
border-radius: 6px;
margin: 16px 0;
}
/* Tiptap specific styling overrides to match prose */
.tiptap p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.tiptap {
outline: none;
}
.tiptap ul[data-type="taskList"] {
list-style: none;
padding: 0;
}
.tiptap ul[data-type="taskList"] li {
display: flex;
align-items: flex-start;
margin-top: 0;
margin-bottom: 0;
}
.tiptap ul[data-type="taskList"] li > label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
margin-top: 0.2rem;
}
.tiptap ul[data-type="taskList"] li > div {
flex: 1 1 auto;
margin: 0;
}
.tiptap ul[data-type="taskList"] li > div > p {
margin: 0;
}
.tiptap p {
margin-top: 0;
margin-bottom: 0.65em;
}
/* Printing logic for PDF export */
@media print {
.tiptap pre,
.markdown-preview pre {
white-space: pre-wrap !important;
word-wrap: break-word !important;
break-inside: avoid !important;
}
.code-block-header {
display: none !important;
}
}
/* Custom Node Views (Code Block) */
.tiptap .code-block-wrapper {
margin-bottom: 0.65em;
border-radius: 6px;
background-color: #0d1117;
overflow: hidden;
}
.tiptap .code-block-wrapper pre {
margin: 0 !important;
padding: 1rem;
border-radius: 0 0 6px 6px;
background: transparent;
}
/* Markdown Content Wrapper Padding Strategies */
.markdown-content-wrapper.is-normal.is-read-mode > .prose {
padding-bottom: 3rem; /* 48px */
}
.markdown-content-wrapper.is-fullscreen.is-read-mode > .prose {
padding-bottom: 12rem; /* 192px (Accounts for 90px banner ad + spacing) */
}
.markdown-content-wrapper.is-normal.is-edit-mode > div {
padding-bottom: 3rem; /* 48px */
}
.markdown-content-wrapper.is-fullscreen.is-edit-mode > div {
padding-bottom: 12rem; /* 192px (Accounts for 90px banner ad + spacing) */
}

View File

@@ -1,34 +1,165 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
darkMode: 'class', // Enable manual dark mode control via class
content: ["./src/**/*.{js,jsx,ts,tsx}"],
darkMode: "class", // Enable manual dark mode control via class
theme: {
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
}
50: "#f0f9ff",
100: "#e0f2fe",
200: "#bae6fd",
300: "#7dd3fc",
400: "#38bdf8",
500: "#0ea5e9",
600: "#0284c7",
700: "#0369a1",
800: "#075985",
900: "#0c4a6e",
},
},
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'],
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%',
}
typography: (theme) => ({
DEFAULT: {
css: {
"--tw-prose-body": "#24292f",
"--tw-prose-headings": "#24292f",
"--tw-prose-lead": "#57606a",
"--tw-prose-links": "#0969da",
"--tw-prose-bold": "#24292f",
"--tw-prose-counters": "#57606a",
"--tw-prose-bullets": "#d0d7de",
"--tw-prose-hr": "#d0d7de",
"--tw-prose-quotes": "#57606a",
"--tw-prose-quote-borders": "#d0d7de",
"--tw-prose-captions": "#57606a",
"--tw-prose-code": "#24292f",
"--tw-prose-pre-code": "#24292f",
"--tw-prose-pre-bg": "#f6f8fa",
"--tw-prose-th-borders": "#d0d7de",
"--tw-prose-td-borders": "#d0d7de",
// Invert colors for dark mode
"--tw-prose-invert-body": "#c9d1d9",
"--tw-prose-invert-headings": "#c9d1d9",
"--tw-prose-invert-lead": "#8b949e",
"--tw-prose-invert-links": "#58a6ff",
"--tw-prose-invert-bold": "#c9d1d9",
"--tw-prose-invert-counters": "#8b949e",
"--tw-prose-invert-bullets": "#30363d",
"--tw-prose-invert-hr": "#21262d",
"--tw-prose-invert-quotes": "#8b949e",
"--tw-prose-invert-quote-borders": "#30363d",
"--tw-prose-invert-captions": "#8b949e",
"--tw-prose-invert-code": "#c9d1d9",
"--tw-prose-invert-pre-code": "#c9d1d9",
"--tw-prose-invert-pre-bg": "#161b22",
"--tw-prose-invert-th-borders": "#30363d",
"--tw-prose-invert-td-borders": "#30363d",
// Adjust margins and sizes (Standardizing to GitHub Markdown / Modern defaults)
maxWidth: "none",
lineHeight: "1.4",
p: {
marginTop: "0",
marginBottom: "0.65em",
},
"h1, h2, h3, h4, h5, h6": {
marginTop: "1em",
marginBottom: "0.65em",
fontWeight: "600",
lineHeight: "1.2",
},
h1: {
fontSize: "2em",
paddingBottom: "0.2em",
borderBottomWidth: "1px",
},
h2: {
fontSize: "1.5em",
paddingBottom: "0.2em",
borderBottomWidth: "1px",
},
h3: { fontSize: "1.25em" },
h4: { fontSize: "1em" },
h5: { fontSize: "0.875em" },
h6: { fontSize: "0.85em", color: "var(--tw-prose-lead)" },
"ul, ol": {
marginTop: "0",
marginBottom: "0.65em",
paddingLeft: "1.5em",
},
li: {
marginTop: "0.15em",
marginBottom: "0.15em",
},
"li > p": {
marginTop: "0",
marginBottom: "0",
},
blockquote: {
marginTop: "0",
marginBottom: "0.65em",
paddingLeft: "1em",
fontStyle: "normal",
borderLeftWidth: "4px",
},
pre: {
marginTop: "0",
marginBottom: "0.65em",
padding: "0.75em",
borderRadius: "6px",
},
code: {
backgroundColor: "rgba(175, 184, 193, 0.2)",
padding: "0.2em 0.4em",
borderRadius: "6px",
fontWeight: "inherit",
},
"code::before": { content: '""' },
"code::after": { content: '""' },
"pre code": {
backgroundColor: "transparent",
padding: "0",
},
table: {
marginTop: "0",
marginBottom: "0.65em",
},
"thead th": {
padding: "0.4em 0.75em",
borderWidth: "1px",
},
"tbody td": {
padding: "0.4em 0.75em",
borderWidth: "1px",
},
hr: {
marginTop: "1em",
marginBottom: "1em",
height: "0.25em",
borderWidth: "0",
backgroundColor: "var(--tw-prose-hr)",
},
},
plugins: [],
}
},
}),
},
},
plugins: [require("@tailwindcss/typography")],
};

View File

@@ -1 +0,0 @@
<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Test Heading</h1><p>Test paragraph</p><div class="container">Test div</div></body></html>