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": [
@@ -291,4 +308,4 @@
]
}
]
}
}

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
@@ -37,25 +45,32 @@ function App() {
<Router>
<Layout>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/url" element={<UrlTool />} />
<Route path="/base64" element={<Base64Tool />} />
<Route path="/beautifier" element={<BeautifierTool />} />
<Route path="/diff" element={<DiffTool />} />
<Route path="/text-length" element={<TextLengthTool />} />
<Route path="/object-editor" element={<ObjectEditor />} />
<Route path="/table-editor" element={<TableEditor />} />
<Route path="/invoice-editor" element={<InvoiceEditor />} />
<Route path="/markdown-editor" element={<MarkdownEditor />} />
<Route path="/invoice-preview" element={<InvoicePreview />} />
<Route path="/invoice-preview-minimal" element={<InvoicePreviewMinimal />} />
<Route path="/whats-new" element={<Navigate to="/release-notes" replace />} />
<Route path="/release-notes" element={<ReleaseNotes />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/url" element={<UrlTool />} />
<Route path="/base64" element={<Base64Tool />} />
<Route path="/beautifier" element={<BeautifierTool />} />
<Route path="/diff" element={<DiffTool />} />
<Route path="/text-length" element={<TextLengthTool />} />
<Route path="/object-editor" element={<ObjectEditor />} />
<Route path="/table-editor" element={<TableEditor />} />
<Route path="/invoice-editor" element={<InvoiceEditor />} />
<Route path="/markdown-editor" element={<MarkdownEditor />} />
<Route path="/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="/release-notes" element={<ReleaseNotes />} />
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/terms" element={<TermsOfService />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</Layout>
</Router>

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,25 +1,37 @@
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);
// Initialize analytics tracking
useAnalytics();
const isActive = (path) => {
return location.pathname === path;
};
@@ -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,33 +57,42 @@ 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">
{/* SEO Head Management */}
<SEOHead />
{/* 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">
<img
src="/logo.svg"
<img
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">
@@ -83,7 +104,7 @@ const Layout = ({ children }) => {
</div>
</div>
</button>
<div className="flex items-center space-x-4">
{/* Desktop Navigation - only show on homepage */}
{!isToolPage && (
@@ -91,18 +112,18 @@ 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" />
<span>Home</span>
</button>
{/* Tools Dropdown */}
<div className="relative" ref={dropdownRef}>
<button
@@ -113,11 +134,13 @@ 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 */}
{isDropdownOpen && (
<div className="absolute top-full left-0 mt-3 w-80 bg-white/90 dark:bg-slate-800/90 backdrop-blur-md rounded-2xl shadow-2xl border border-slate-200/50 dark:border-slate-700/50 py-3 z-50 overflow-hidden">
@@ -125,8 +148,10 @@ 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
key={tool.path}
@@ -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" />
@@ -159,9 +188,9 @@ const Layout = ({ children }) => {
</div>
</nav>
)}
<ThemeToggle />
{/* Mobile Menu Button */}
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
@@ -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>
@@ -180,49 +213,19 @@ const Layout = ({ children }) => {
{isMobileMenuOpen && (
<>
{/* Overlay */}
<div
<div
className="md:hidden fixed inset-0 bg-black/20 z-30"
onClick={() => setIsMobileMenuOpen(false)}
/>
{/* Menu */}
<div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="space-y-2">
{/* Non-Tools Section */}
{NON_TOOLS.map((tool) => {
const IconComponent = tool.icon;
return (
<button
key={tool.path}
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<div className={`p-2 rounded-lg ${isActive(tool.path) ? 'bg-white/20' : 'bg-gradient-to-br from-indigo-500 to-purple-500'} shadow-sm`}>
<IconComponent className={`h-4 w-4 ${isActive(tool.path) ? 'text-white' : 'text-white'}`} />
</div>
<span>{tool.name}</span>
</button>
);
})}
<div className="border-t border-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'}
</div>
{TOOLS.map((tool) => {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="space-y-2">
{/* Non-Tools Section */}
{NON_TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return (
<button
key={tool.path}
@@ -230,31 +233,69 @@ const Layout = ({ children }) => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
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-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-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 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={`p-2 rounded-lg ${isActive(tool.path) ? "bg-white/20" : "bg-gradient-to-br from-indigo-500 to-purple-500"} shadow-sm`}
>
<IconComponent
className={`h-4 w-4 ${isActive(tool.path) ? "text-white" : "text-white"}`}
/>
</div>
<span>{tool.name}</span>
</button>
);
})}
<div className="border-t border-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"}
</div>
{TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return (
<button
key={tool.path}
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? "bg-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`}
>
<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>
</button>
);
})}
</div>
</div>
</div>
</div>
</div>
</>
)}
{/* 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">
@@ -287,15 +324,15 @@ const Layout = ({ children }) => {
<div className="relative">
<div className="absolute inset-0 rounded-lg blur opacity-20"></div>
<div className="relative p-2">
<img
src="/icon-192x192.png"
<img
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">
@@ -333,22 +370,26 @@ const Layout = ({ children }) => {
</div>
</div>
<div className="flex items-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
<button
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')}
<span className="text-slate-300 dark:text-slate-600">
</span>
<button
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')}
<span className="text-slate-300 dark:text-slate-600">
</span>
<button
onClick={() => navigateWithGuard("/terms")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
@@ -369,15 +410,15 @@ const Layout = ({ children }) => {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<img
src="/icon-192x192.png"
<img
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>
@@ -389,22 +430,22 @@ const Layout = ({ children }) => {
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<div className="flex items-center justify-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
<button
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')}
<button
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')}
<button
onClick={() => navigateWithGuard("/terms")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
@@ -414,7 +455,7 @@ const Layout = ({ children }) => {
</div>
</footer>
)}
{/* GDPR Consent Banner */}
<ConsentBanner />

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,33 +39,21 @@ 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">
<iframe
ref={iframeRef}
style={{ width: "320px", height: "50px", border: "none" }}
title="Mobile Advertisement"
sandbox="allow-scripts allow-same-origin"
/>
</div>
<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",
display: "block",
}}
title="Mobile Advertisement"
sandbox="allow-scripts allow-same-origin"
/>
</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;

File diff suppressed because it is too large Load Diff

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,69 +2,75 @@
@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;
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;
}
html {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
width: 100%;
max-width: 100vw;
}
body {
width: 100%;
max-width: 100vw;
}
#root {
width: 100%;
max-width: 100vw;
min-width: 0;
}
code,
pre {
font-family:
"JetBrains Mono", Monaco, "Cascadia Code", "Segoe UI Mono",
"Roboto Mono", monospace;
}
}
@layer components {
.tool-card {
@apply bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 hover:shadow-md transition-shadow duration-200;
}
.tool-input {
@apply w-full p-3 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 font-mono text-sm resize-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;
}
.tool-button {
@apply px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed;
}
.tool-button-secondary {
@apply px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md font-medium transition-colors duration-200;
}
.tool-button-primary {
@apply flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md font-medium transition-colors duration-200;
}
.toolbar-btn {
@apply p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 font-medium text-sm;
}
.copy-button {
@apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200;
}
.scrollbar-hide {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
.tool-card {
@apply bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 hover:shadow-md transition-shadow duration-200;
}
.tool-input {
@apply w-full p-3 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 font-mono text-sm resize-none focus:ring-2 focus:ring-primary-500 focus:border-transparent;
}
.tool-button {
@apply px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed;
}
.tool-button-secondary {
@apply px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-md font-medium transition-colors duration-200;
}
.tool-button-primary {
@apply flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md font-medium transition-colors duration-200;
}
.toolbar-btn {
@apply p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 font-medium text-sm;
}
.copy-button {
@apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200;
}
.scrollbar-hide {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
}

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