Compare commits
15 Commits
6a14eebf25
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1047642909 | ||
|
|
8caf6fbba5 | ||
|
|
3727ace366 | ||
|
|
81c399ab42 | ||
|
|
518b0127d2 | ||
|
|
deb2bf0b8a | ||
|
|
e4ccff4bbf | ||
|
|
0b1cfbdabd | ||
|
|
c580a5f7b0 | ||
|
|
9232052508 | ||
|
|
fcbfeb44f8 | ||
|
|
dd0b98e077 | ||
|
|
dcba58c2b9 | ||
|
|
7b3dce06ea | ||
|
|
13e694aa82 |
BIN
._node_modules
BIN
._node_modules
Binary file not shown.
BIN
._package-lock.json
generated
BIN
._package-lock.json
generated
Binary file not shown.
BIN
._package.json
BIN
._package.json
Binary file not shown.
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,7 +11,6 @@ node_modules/
|
|||||||
/backup
|
/backup
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
@@ -46,4 +45,6 @@ pids
|
|||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
# OS generated files
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
._*
|
||||||
|
|||||||
@@ -131,37 +131,36 @@ Build a comprehensive suite of developer tools with a focus on:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Priority 3: AdSense Integration 💵
|
#### Priority 3: Adsterra Integration 💵
|
||||||
**Status:** ⏳ In Progress (Awaiting approval)
|
**Status:** ✅ Completed
|
||||||
**Timeline:** 1 day
|
**Timeline:** 1 day
|
||||||
**Impact:** HIGH - Start earning revenue
|
**Impact:** HIGH - Start earning revenue
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
1. Apply for Google AdSense account
|
1. ✅ Apply for Adsterra account
|
||||||
2. Add AdSense script to `index.html`
|
2. ✅ Add Adsterra anti-adblock script to `index.html` and components
|
||||||
3. Create ad units in AdSense dashboard
|
3. ✅ Create ad units in Adsterra dashboard
|
||||||
4. Implement ad components with AdSense code
|
4. ✅ Implement ad components with Adsterra code
|
||||||
5. Test ad display and responsiveness
|
5. ✅ Test ad display and responsiveness
|
||||||
6. Monitor ad performance
|
|
||||||
|
|
||||||
**Ad Units Needed:**
|
**Ad Units Needed:**
|
||||||
- Desktop Sidebar 1 (300x250)
|
- ✅ Desktop Sidebar 1 (300x250)
|
||||||
- Desktop Sidebar 2 (300x250)
|
- ✅ Desktop Sidebar 2 (300x250)
|
||||||
- Desktop Sidebar 3 (300x250)
|
- ✅ Desktop Sidebar 3 (300x250)
|
||||||
- Mobile Bottom Banner (320x50)
|
- ✅ Mobile Bottom Banner (320x50)
|
||||||
|
|
||||||
**Compliance:**
|
**Compliance:**
|
||||||
- Add Privacy Policy page
|
- ✅ Add Privacy Policy page
|
||||||
- Add Terms of Service page
|
- ✅ Add Terms of Service page
|
||||||
- Cookie consent banner (if required)
|
- ✅ Cookie consent banner
|
||||||
- GDPR compliance (if applicable)
|
- ✅ GDPR compliance
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 2: Content Expansion (Week 3-6)
|
### Phase 2: Content Expansion (Week 3-6)
|
||||||
|
|
||||||
#### Markdown Editor 📝
|
#### Markdown Editor 📝
|
||||||
**Status:** ✅ Completed (October 22, 2025)
|
**Status:** ✅ Completed (June 14, 2026)
|
||||||
**Timeline:** 1-2 weeks
|
**Timeline:** 1-2 weeks
|
||||||
**Impact:** HIGH - Major new feature, attracts new users
|
**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)
|
- Create New (empty/sample)
|
||||||
- URL Import (fetch markdown from GitHub, Gist, etc.)
|
- URL Import (fetch markdown from GitHub, Gist, etc.)
|
||||||
- Paste (markdown, HTML auto-convert, plain text)
|
- Paste (markdown, HTML auto-convert, plain text)
|
||||||
- Open Files (.md, .txt, .html, .docx)
|
- Open Files (.md, .txt)
|
||||||
|
|
||||||
- **Editor:**
|
- **Editor:**
|
||||||
- CodeMirror with markdown syntax highlighting
|
- Tiptap-powered WYSIWYG Rich Text Editor
|
||||||
- Split view (editor + live preview)
|
- Fallback Raw Markdown CodeMirror Editor
|
||||||
- View modes: Split, Editor Only, Preview Only, Fullscreen
|
- View modes: Read, Edit, Markdown
|
||||||
- Markdown toolbar (Bold, Italic, Headers, Links, Images, Code, Lists, Tables)
|
- Toolbar (Bold, Italic, Headers, Links, Images, Code, Lists, Tables)
|
||||||
- Line numbers
|
|
||||||
- Word count & statistics
|
- Word count & statistics
|
||||||
|
|
||||||
- **Preview:**
|
- **Preview:**
|
||||||
- Live rendering (marked + DOMPurify)
|
- Auto-generated HTML parsing
|
||||||
- Syntax highlighting for code blocks (highlight.js)
|
- Syntax highlighting for code blocks (highlight.js)
|
||||||
- GitHub Flavored Markdown support
|
|
||||||
- Table of Contents auto-generation
|
|
||||||
- Mermaid diagram rendering (in preview)
|
|
||||||
|
|
||||||
- **Export:**
|
- **Export:**
|
||||||
- Markdown (.md) - Standard, GFM, CommonMark
|
- Markdown (.md)
|
||||||
- HTML (.html) - Standalone with CSS
|
- HTML (.html)
|
||||||
|
- HTML Content Body
|
||||||
- Plain Text (.txt)
|
- Plain Text (.txt)
|
||||||
- PDF (.pdf) - via html2pdf
|
- PDF (.pdf) - via html2pdf
|
||||||
- DOCX (.docx) - via docx library
|
|
||||||
|
|
||||||
**Advanced Features (Post-MVP):**
|
**Advanced Features (Post-MVP):**
|
||||||
- Tables support (GitHub-style)
|
- Tables support (GitHub-style)
|
||||||
|
|||||||
368
TODO.md
368
TODO.md
@@ -179,310 +179,168 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 💵 Priority 3: AdSense Integration (1 day) - ⏳ IN PROGRESS
|
### 💵 Priority 3: Adsterra Integration (1 day) - ✅ COMPLETED
|
||||||
|
|
||||||
#### AdSense Setup
|
#### Adsterra Setup
|
||||||
- [ ] Apply for Google AdSense account
|
- [x] Apply for Adsterra publisher account
|
||||||
- [ ] Provide website URL
|
- [x] Add website URL
|
||||||
- [ ] Provide contact information
|
- [x] Receive approval
|
||||||
- [ ] Wait for approval (can take 1-3 days)
|
|
||||||
- [ ] Verify site ownership (add verification code)
|
|
||||||
|
|
||||||
#### Ad Units Creation
|
#### Ad Units Creation
|
||||||
- [ ] Log in to AdSense dashboard
|
- [x] Create ad unit: Desktop Sidebar 1 (300x250)
|
||||||
- [ ] Create ad unit: Desktop Sidebar 1 (300x250)
|
- [x] Create ad unit: Desktop Sidebar 2 (300x250)
|
||||||
- [ ] Create ad unit: Desktop Sidebar 2 (300x250)
|
- [x] Create ad unit: Mobile Bottom Banner (320x50)
|
||||||
- [ ] Create ad unit: Desktop Sidebar 3 (300x250)
|
- [x] Copy ad unit codes
|
||||||
- [ ] Create ad unit: Mobile Bottom Banner (320x50)
|
- [x] Request Anti-Adblock custom domain
|
||||||
- [ ] Copy ad unit codes
|
|
||||||
|
|
||||||
#### Implementation
|
#### Implementation
|
||||||
- [ ] Add AdSense script to `public/index.html`
|
- [x] Update `AdBlock.jsx` with Adsterra iframe code
|
||||||
```html
|
- [x] Update `MobileAdBanner.jsx` with Adsterra iframe code
|
||||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXX"
|
- [x] Update custom Anti-Adblock domain (`downconvenientmagnetic.com`)
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
```
|
|
||||||
- [ ] Update `AdBlock.jsx` with AdSense code
|
|
||||||
```jsx
|
|
||||||
<ins className="adsbygoogle"
|
|
||||||
style={{ display: 'block' }}
|
|
||||||
data-ad-client="ca-pub-XXXXXXXX"
|
|
||||||
data-ad-slot="XXXXXXXXXX"
|
|
||||||
data-ad-format="auto"
|
|
||||||
data-full-width-responsive="true"></ins>
|
|
||||||
```
|
|
||||||
- [ ] Update `MobileAdBanner.jsx` with AdSense code
|
|
||||||
- [ ] Initialize ads: `(adsbygoogle = window.adsbygoogle || []).push({});`
|
|
||||||
|
|
||||||
#### Testing
|
#### Testing
|
||||||
- [ ] Test ad display on desktop
|
- [x] Test ad display on desktop
|
||||||
- [ ] Test ad display on mobile
|
- [x] Test ad display on mobile
|
||||||
- [ ] Verify ads load correctly
|
- [x] Verify ads load correctly
|
||||||
- [ ] Check for console errors
|
- [x] Check for console errors
|
||||||
- [ ] Test with ad blocker (should show message)
|
- [x] Test on different devices
|
||||||
- [ ] Test on different browsers (Chrome, Firefox, Safari)
|
|
||||||
- [ ] Test on different devices
|
|
||||||
|
|
||||||
#### Monitoring
|
#### Monitoring
|
||||||
- [ ] Set up AdSense reporting
|
- [x] Monitor ad impressions
|
||||||
- [ ] Monitor ad impressions
|
- [x] Monitor ad clicks
|
||||||
- [ ] Monitor ad clicks
|
- [x] Track CTR (Click-Through Rate)
|
||||||
- [ ] Monitor ad revenue
|
|
||||||
- [ ] Track CTR (Click-Through Rate)
|
|
||||||
- [ ] Identify best-performing ad units
|
|
||||||
|
|
||||||
#### Compliance
|
#### Compliance
|
||||||
- [ ] Create Privacy Policy page
|
- [x] Create Privacy Policy page
|
||||||
- [ ] Data collection disclosure
|
- [x] Data collection disclosure
|
||||||
- [ ] Cookie usage disclosure
|
- [x] Cookie usage disclosure
|
||||||
- [ ] Third-party services (AdSense)
|
- [x] Third-party services (Adsterra)
|
||||||
- [ ] User rights (GDPR)
|
- [x] User rights (GDPR)
|
||||||
- [ ] Create Terms of Service page
|
- [x] Create Terms of Service page
|
||||||
- [ ] Acceptable use policy
|
- [x] Acceptable use policy
|
||||||
- [ ] Disclaimer
|
- [x] Disclaimer
|
||||||
- [ ] Limitation of liability
|
- [x] Limitation of liability
|
||||||
- [ ] Add cookie consent banner (if required)
|
- [x] Add cookie consent banner
|
||||||
- [ ] Show on first visit
|
- [x] Add "Privacy Policy" link in footer
|
||||||
- [ ] Allow accept/decline
|
- [x] Add "Terms of Service" link in footer
|
||||||
- [ ] Store preference
|
|
||||||
- [ ] Add "About Ads" link in footer
|
|
||||||
- [ ] Add "Privacy Policy" link in footer
|
|
||||||
- [ ] Add "Terms of Service" link in footer
|
|
||||||
|
|
||||||
#### Optimization
|
|
||||||
- [ ] Test different ad placements
|
|
||||||
- [ ] Test different ad sizes
|
|
||||||
- [ ] Monitor ad viewability
|
|
||||||
- [ ] Optimize for higher CTR
|
|
||||||
- [ ] A/B test ad positions (optional)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 Phase 2: Content Expansion
|
## 📋 Phase 2: Content Expansion
|
||||||
|
|
||||||
### 📝 Markdown Editor - MVP (1-2 weeks) - ✅ COMPLETED (Oct 22, 2025)
|
### 📝 Markdown Editor - MVP (1-2 weeks) - ✅ COMPLETED
|
||||||
|
|
||||||
#### Planning
|
#### Planning
|
||||||
- [ ] Finalize feature list for MVP
|
- [x] Finalize feature list for MVP
|
||||||
- [ ] Design UI mockup (split view)
|
- [x] Design UI mockup (WYSIWYG layout)
|
||||||
- [ ] Plan component structure
|
- [x] Plan component structure
|
||||||
- [ ] Choose markdown parser (marked vs markdown-it)
|
- [x] Implement Tiptap integration
|
||||||
- [ ] Plan export formats
|
- [x] Plan export formats
|
||||||
|
|
||||||
#### Project Setup
|
#### Project Setup
|
||||||
- [ ] Create `MarkdownEditor.jsx` page
|
- [x] Create `MarkdownEditor.jsx` page
|
||||||
- [ ] Set up routing (`/markdown-editor`)
|
- [x] Create `RichMarkdownEditor.js` component
|
||||||
- [ ] Add to navigation menu
|
- [x] Set up routing (`/markdown-editor`)
|
||||||
- [ ] Add to homepage tools list
|
- [x] Add to navigation menu
|
||||||
|
- [x] Add to homepage tools list
|
||||||
|
|
||||||
#### Input Section
|
#### Input Section
|
||||||
- [ ] Implement Create New tab
|
- [x] Implement Create New tab
|
||||||
- [ ] Start Empty button
|
- [x] Start Empty button
|
||||||
- [ ] Load Sample button (with example markdown)
|
- [x] Load Sample button (with example markdown)
|
||||||
- [ ] Tip box
|
- [x] Tip box
|
||||||
- [ ] Implement URL tab
|
- [x] Implement URL tab
|
||||||
- [ ] Use AdvancedURLFetch component
|
- [x] Use AdvancedURLFetch component
|
||||||
- [ ] Support GitHub raw URLs
|
- [x] Support GitHub raw URLs
|
||||||
- [ ] Support Gist URLs
|
- [x] Support Gist URLs
|
||||||
- [ ] Test with various markdown sources
|
- [x] Test with various markdown sources
|
||||||
- [ ] Implement Paste tab
|
- [x] Implement Paste tab
|
||||||
- [ ] CodeMirror editor
|
- [x] CodeMirror editor
|
||||||
- [ ] Markdown syntax highlighting
|
- [x] Markdown syntax highlighting
|
||||||
- [ ] Auto-detect markdown
|
- [x] Parse button
|
||||||
- [ ] Parse button
|
- [x] Implement Open tab
|
||||||
- [ ] Collapse after parse
|
- [x] Support .md files
|
||||||
- [ ] Implement Open tab
|
- [x] Support .txt files
|
||||||
- [ ] Support .md files
|
- [x] Auto-load on file selection
|
||||||
- [ ] Support .txt files
|
|
||||||
- [ ] Support .html files (convert to markdown)
|
|
||||||
- [ ] Support .docx files (convert to markdown)
|
|
||||||
- [ ] Auto-load on file selection
|
|
||||||
|
|
||||||
#### Editor Section
|
#### Editor Section
|
||||||
- [ ] Set up CodeMirror for markdown
|
- [x] Implement WYSIWYG Editor (Tiptap)
|
||||||
- [ ] Install @codemirror/lang-markdown
|
- [x] Install `@tiptap/react` and `tiptap-markdown`
|
||||||
- [ ] Configure markdown mode
|
- [x] Add standard text formatting (bold, italic, strike)
|
||||||
- [ ] Add syntax highlighting
|
- [x] Add block formatting (headers, quotes, lists)
|
||||||
- [ ] Add line numbers
|
- [x] Add inline code and code block extensions
|
||||||
- [ ] Add line wrapping
|
- [x] Set up Lowlight syntax highlighting
|
||||||
- [ ] Implement split view layout
|
- [x] Implement view mode toggle
|
||||||
- [ ] Editor pane (left)
|
- [x] Read mode (Clean preview default)
|
||||||
- [ ] Preview pane (right)
|
- [x] Edit mode (WYSIWYG Tiptap)
|
||||||
- [ ] Resizable divider (optional)
|
- [x] Markdown mode (Raw CodeMirror)
|
||||||
- [ ] Implement view mode toggle
|
- [x] Fullscreen mode
|
||||||
- [ ] Split view (default)
|
- [x] Add editor features
|
||||||
- [ ] Editor only
|
- [x] Word count
|
||||||
- [ ] Preview only
|
- [x] Character count
|
||||||
- [ ] Fullscreen mode
|
- [x] Line count
|
||||||
- [ ] Add markdown toolbar
|
- [x] Reading time estimate
|
||||||
- [ ] Bold button (Ctrl+B)
|
|
||||||
- [ ] Italic button (Ctrl+I)
|
|
||||||
- [ ] H1 button
|
|
||||||
- [ ] H2 button
|
|
||||||
- [ ] H3 button
|
|
||||||
- [ ] Link button (Ctrl+K)
|
|
||||||
- [ ] Image button
|
|
||||||
- [ ] Code button (Ctrl+`)
|
|
||||||
- [ ] Quote button
|
|
||||||
- [ ] Unordered list button
|
|
||||||
- [ ] Ordered list button
|
|
||||||
- [ ] Table button
|
|
||||||
- [ ] Add editor features
|
|
||||||
- [ ] Word count
|
|
||||||
- [ ] Character count
|
|
||||||
- [ ] Line count
|
|
||||||
- [ ] Reading time estimate
|
|
||||||
|
|
||||||
#### Preview Section
|
#### Preview Section
|
||||||
- [ ] Set up markdown parser (marked)
|
- [x] Build robust HTML to Markdown / Markdown to HTML sync
|
||||||
- [ ] Install marked
|
- [x] Set up markdown fallback parser (marked)
|
||||||
- [ ] Install DOMPurify
|
- [x] GitHub Flavored Markdown support (Tables, task lists)
|
||||||
- [ ] Configure marked options
|
- [x] Custom code block rendering with Copy button in Read mode
|
||||||
- [ ] Implement live preview
|
|
||||||
- [ ] Real-time rendering
|
|
||||||
- [ ] Debounce for performance
|
|
||||||
- [ ] Scroll sync (optional)
|
|
||||||
- [ ] Add syntax highlighting for code blocks
|
|
||||||
- [ ] Install highlight.js
|
|
||||||
- [ ] Configure languages
|
|
||||||
- [ ] Apply highlighting
|
|
||||||
- [ ] Add GitHub Flavored Markdown support
|
|
||||||
- [ ] Tables
|
|
||||||
- [ ] Strikethrough
|
|
||||||
- [ ] Task lists
|
|
||||||
- [ ] Autolinks
|
|
||||||
- [ ] Implement Table of Contents
|
|
||||||
- [ ] Auto-generate from headers
|
|
||||||
- [ ] Clickable links
|
|
||||||
- [ ] Collapsible (optional)
|
|
||||||
- [ ] Add mermaid diagram rendering
|
|
||||||
- [ ] Install mermaid
|
|
||||||
- [ ] Detect mermaid code blocks
|
|
||||||
- [ ] Render diagrams
|
|
||||||
- [ ] Error handling
|
|
||||||
|
|
||||||
#### Export Section
|
#### Export Section
|
||||||
- [ ] Create collapsible export section
|
- [x] Create collapsible export section
|
||||||
- [ ] Implement Markdown export
|
- [x] Implement Markdown export
|
||||||
- [ ] Standard Markdown
|
- [x] Copy to clipboard
|
||||||
- [ ] GitHub Flavored Markdown
|
- [x] Download as .md file
|
||||||
- [ ] CommonMark
|
- [x] Implement HTML export
|
||||||
- [ ] Copy to clipboard
|
- [x] Standalone HTML with CSS
|
||||||
- [ ] Download as .md file
|
- [x] Download as .html file
|
||||||
- [ ] Implement HTML export
|
- [x] Implement HTML Content export
|
||||||
- [ ] Standalone HTML with CSS
|
- [x] Strip React/Tailwind wrapper classes
|
||||||
- [ ] Inline styles
|
- [x] Download body HTML only
|
||||||
- [ ] Include syntax highlighting CSS
|
- [x] Implement Plain Text export
|
||||||
- [ ] Copy to clipboard
|
- [x] Strip markdown syntax via regex
|
||||||
- [ ] Download as .html file
|
- [x] Download as .txt file
|
||||||
- [ ] Implement Plain Text export
|
- [x] Implement PDF export
|
||||||
- [ ] Strip all formatting
|
- [x] Install html2pdf.js
|
||||||
- [ ] Copy to clipboard
|
- [x] Inject CSS print media rules to prevent pre overflow
|
||||||
- [ ] Download as .txt file
|
- [x] Download as .pdf file
|
||||||
- [ ] Implement PDF export
|
|
||||||
- [ ] Install html2pdf.js
|
|
||||||
- [ ] Convert HTML to PDF
|
|
||||||
- [ ] Maintain formatting
|
|
||||||
- [ ] Download as .pdf file
|
|
||||||
- [ ] Implement DOCX export
|
|
||||||
- [ ] Install docx library
|
|
||||||
- [ ] Convert markdown to DOCX
|
|
||||||
- [ ] Maintain formatting
|
|
||||||
- [ ] Download as .docx file
|
|
||||||
|
|
||||||
#### Conversion Features
|
|
||||||
- [ ] HTML to Markdown conversion
|
|
||||||
- [ ] Install turndown
|
|
||||||
- [ ] Convert on paste (if HTML detected)
|
|
||||||
- [ ] Convert on file open (.html)
|
|
||||||
- [ ] DOCX to Markdown conversion
|
|
||||||
- [ ] Install mammoth.js
|
|
||||||
- [ ] Convert on file open (.docx)
|
|
||||||
- [ ] Extract text and formatting
|
|
||||||
|
|
||||||
#### Usage Tips
|
|
||||||
- [ ] Create collapsible Usage Tips section
|
|
||||||
- [ ] Add Input Methods tips
|
|
||||||
- [ ] Add Editor Features tips
|
|
||||||
- [ ] Add Markdown Syntax tips
|
|
||||||
- [ ] Add Export Options tips
|
|
||||||
- [ ] Add Data Privacy tips
|
|
||||||
|
|
||||||
#### Data Loss Prevention
|
#### Data Loss Prevention
|
||||||
- [ ] Implement hasUserData() function
|
- [x] Implement `hasUserData()` function
|
||||||
- [ ] Implement hasModifiedData() function
|
- [x] Implement `hasModifiedData()` function
|
||||||
- [ ] Add confirmation modal for tab changes
|
- [x] Add confirmation modal for tab changes
|
||||||
- [ ] Add confirmation for Create New buttons
|
- [x] Add confirmation for Create New buttons
|
||||||
|
|
||||||
#### Testing
|
#### Testing
|
||||||
- [ ] Test all input methods
|
- [x] Test all input methods
|
||||||
- [ ] Test markdown rendering
|
- [x] Test Tiptap to Markdown serialization
|
||||||
- [ ] Test all export formats
|
- [x] Test all export formats
|
||||||
- [ ] Test HTML to Markdown conversion
|
- [x] Test code syntax highlighting
|
||||||
- [ ] Test DOCX import
|
- [x] Test view mode toggle
|
||||||
- [ ] Test mermaid diagrams
|
- [x] Test toolbar buttons
|
||||||
- [ ] Test code syntax highlighting
|
- [x] Test responsive design
|
||||||
- [ ] Test Table of Contents
|
- [x] Test dark mode
|
||||||
- [ ] Test view mode toggle
|
|
||||||
- [ ] Test toolbar buttons
|
|
||||||
- [ ] Test keyboard shortcuts
|
|
||||||
- [ ] Test responsive design
|
|
||||||
- [ ] Test dark mode
|
|
||||||
- [ ] Test on mobile devices
|
|
||||||
|
|
||||||
#### Documentation
|
|
||||||
- [ ] Add to EDITOR_TOOL_GUIDE.md
|
|
||||||
- [ ] Create user guide
|
|
||||||
- [ ] Add screenshots
|
|
||||||
- [ ] Create tutorial video (optional)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 📝 Markdown Editor - Post-MVP (Future)
|
### 📝 Markdown Editor - Post-MVP (Future)
|
||||||
|
|
||||||
#### Advanced Markdown Features
|
#### Advanced Markdown Features
|
||||||
- [ ] Add table support (GitHub-style)
|
|
||||||
- [ ] Add task lists (checkboxes)
|
|
||||||
- [ ] Add footnotes support
|
- [ ] Add footnotes support
|
||||||
- [ ] Add emoji support (:smile:)
|
- [ ] Add emoji support (WYSIWYG picker)
|
||||||
- [ ] Add math equations (KaTeX)
|
- [ ] Add math equations (KaTeX)
|
||||||
- [ ] Install katex
|
- [ ] Add mermaid diagram rendering
|
||||||
- [ ] Detect math blocks
|
- [ ] Implement Table of Contents auto-generation
|
||||||
- [ ] Render equations
|
|
||||||
|
|
||||||
#### Templates
|
|
||||||
- [ ] Create README.md template
|
|
||||||
- [ ] Create Documentation template
|
|
||||||
- [ ] Create Blog post template
|
|
||||||
- [ ] Create Meeting notes template
|
|
||||||
- [ ] Create Project proposal template
|
|
||||||
- [ ] Add template selector UI
|
|
||||||
- [ ] Allow custom templates
|
|
||||||
|
|
||||||
#### Utilities
|
#### Utilities
|
||||||
- [ ] Add markdown linter
|
- [ ] 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
|
- [ ] Add format beautifier
|
||||||
- [ ] Clean up markdown
|
|
||||||
- [ ] Consistent formatting
|
|
||||||
- [ ] Fix indentation
|
|
||||||
- [ ] Add image optimizer
|
- [ ] Add image optimizer
|
||||||
- [ ] Compress images
|
|
||||||
- [ ] Convert to base64
|
|
||||||
- [ ] Optimize for web
|
|
||||||
|
|
||||||
#### Enhanced Features
|
#### Enhanced Features
|
||||||
- [ ] Add keyboard shortcuts
|
|
||||||
- [ ] Add auto-save (localStorage)
|
- [ ] Add auto-save (localStorage)
|
||||||
- [ ] Add export history
|
|
||||||
- [ ] Add version history
|
- [ ] Add version history
|
||||||
- [ ] Add collaborative editing (future)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
2425
package-lock.json
generated
2425
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -18,22 +18,37 @@
|
|||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@testing-library/jest-dom": "^6.8.0",
|
"@testing-library/jest-dom": "^6.8.0",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@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",
|
"@uiw/react-codemirror": "^4.25.1",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"dompurify": "^3.3.0",
|
"dompurify": "^3.3.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"html2pdf.js": "^0.12.1",
|
"html2pdf.js": "^0.12.1",
|
||||||
"js-beautify": "^1.15.4",
|
"js-beautify": "^1.15.4",
|
||||||
"jspdf": "^3.0.3",
|
"jspdf": "^3.0.3",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
"lucide-react": "^0.540.0",
|
"lucide-react": "^0.540.0",
|
||||||
"marked": "^16.4.1",
|
"marked": "^16.4.1",
|
||||||
"marked-emoji": "^2.0.1",
|
"marked-emoji": "^2.0.1",
|
||||||
|
"mermaid": "^11.15.0",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-diff-view": "^3.3.2",
|
"react-diff-view": "^3.3.2",
|
||||||
@@ -41,9 +56,12 @@
|
|||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-router-dom": "6.26.2",
|
"react-router-dom": "6.26.2",
|
||||||
"react-snap": "^1.23.0",
|
"react-snap": "^1.23.0",
|
||||||
|
"react-zoom-pan-pinch": "^4.0.3",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"serialize-javascript": "^6.0.0",
|
"serialize-javascript": "^6.0.0",
|
||||||
"serve": "^14.2.4",
|
"serve": "^14.2.4",
|
||||||
|
"tailwindcss-typography": "^3.1.0",
|
||||||
|
"tiptap-markdown": "^0.9.0",
|
||||||
"turndown": "^7.2.1",
|
"turndown": "^7.2.1",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"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",
|
"date": "2026-02-18",
|
||||||
"changes": [
|
"changes": [
|
||||||
@@ -291,4 +308,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/App.js
101
src/App.js
@@ -1,29 +1,37 @@
|
|||||||
import React, { useEffect, Suspense, lazy } from 'react';
|
import React, { useEffect, Suspense, lazy } from "react";
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import {
|
||||||
import { HelmetProvider } from 'react-helmet-async';
|
BrowserRouter as Router,
|
||||||
import Layout from './components/Layout';
|
Routes,
|
||||||
import ErrorBoundary from './components/ErrorBoundary';
|
Route,
|
||||||
import Loading from './components/Loading';
|
Navigate,
|
||||||
import { initGA } from './utils/analytics';
|
} 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 Home = lazy(() => import("./pages/Home"));
|
||||||
const UrlTool = lazy(() => import('./pages/UrlTool'));
|
const UrlTool = lazy(() => import("./pages/UrlTool"));
|
||||||
const Base64Tool = lazy(() => import('./pages/Base64Tool'));
|
const Base64Tool = lazy(() => import("./pages/Base64Tool"));
|
||||||
const BeautifierTool = lazy(() => import('./pages/BeautifierTool'));
|
const BeautifierTool = lazy(() => import("./pages/BeautifierTool"));
|
||||||
const DiffTool = lazy(() => import('./pages/DiffTool'));
|
const DiffTool = lazy(() => import("./pages/DiffTool"));
|
||||||
const TextLengthTool = lazy(() => import('./pages/TextLengthTool'));
|
const TextLengthTool = lazy(() => import("./pages/TextLengthTool"));
|
||||||
const ObjectEditor = lazy(() => import('./pages/ObjectEditor'));
|
const ObjectEditor = lazy(() => import("./pages/ObjectEditor"));
|
||||||
const TableEditor = lazy(() => import('./pages/TableEditor'));
|
const TableEditor = lazy(() => import("./pages/TableEditor"));
|
||||||
const InvoiceEditor = lazy(() => import('./pages/InvoiceEditor'));
|
const InvoiceEditor = lazy(() => import("./pages/InvoiceEditor"));
|
||||||
const MarkdownEditor = lazy(() => import('./pages/MarkdownEditor'));
|
const MarkdownEditor = lazy(() => import("./pages/MarkdownEditor"));
|
||||||
const InvoicePreview = lazy(() => import('./pages/InvoicePreview'));
|
const DiagramEditor = lazy(() => import("./pages/DiagramEditor"));
|
||||||
const InvoicePreviewMinimal = lazy(() => import('./pages/InvoicePreviewMinimal'));
|
const InvoicePreview = lazy(() => import("./pages/InvoicePreview"));
|
||||||
const ReleaseNotes = lazy(() => import('./pages/ReleaseNotes'));
|
const InvoicePreviewMinimal = lazy(
|
||||||
const TermsOfService = lazy(() => import('./pages/TermsOfService'));
|
() => import("./pages/InvoicePreviewMinimal"),
|
||||||
const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy'));
|
);
|
||||||
const NotFound = lazy(() => import('./pages/NotFound'));
|
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() {
|
function App() {
|
||||||
// Initialize Google Analytics on app startup
|
// Initialize Google Analytics on app startup
|
||||||
@@ -37,25 +45,32 @@ function App() {
|
|||||||
<Router>
|
<Router>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/url" element={<UrlTool />} />
|
<Route path="/url" element={<UrlTool />} />
|
||||||
<Route path="/base64" element={<Base64Tool />} />
|
<Route path="/base64" element={<Base64Tool />} />
|
||||||
<Route path="/beautifier" element={<BeautifierTool />} />
|
<Route path="/beautifier" element={<BeautifierTool />} />
|
||||||
<Route path="/diff" element={<DiffTool />} />
|
<Route path="/diff" element={<DiffTool />} />
|
||||||
<Route path="/text-length" element={<TextLengthTool />} />
|
<Route path="/text-length" element={<TextLengthTool />} />
|
||||||
<Route path="/object-editor" element={<ObjectEditor />} />
|
<Route path="/object-editor" element={<ObjectEditor />} />
|
||||||
<Route path="/table-editor" element={<TableEditor />} />
|
<Route path="/table-editor" element={<TableEditor />} />
|
||||||
<Route path="/invoice-editor" element={<InvoiceEditor />} />
|
<Route path="/invoice-editor" element={<InvoiceEditor />} />
|
||||||
<Route path="/markdown-editor" element={<MarkdownEditor />} />
|
<Route path="/markdown-editor" element={<MarkdownEditor />} />
|
||||||
<Route path="/invoice-preview" element={<InvoicePreview />} />
|
<Route path="/diagram-editor" element={<DiagramEditor />} />
|
||||||
<Route path="/invoice-preview-minimal" element={<InvoicePreviewMinimal />} />
|
<Route path="/invoice-preview" element={<InvoicePreview />} />
|
||||||
<Route path="/whats-new" element={<Navigate to="/release-notes" replace />} />
|
<Route
|
||||||
<Route path="/release-notes" element={<ReleaseNotes />} />
|
path="/invoice-preview-minimal"
|
||||||
<Route path="/privacy" element={<PrivacyPolicy />} />
|
element={<InvoicePreviewMinimal />}
|
||||||
<Route path="/terms" element={<TermsOfService />} />
|
/>
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route
|
||||||
</Routes>
|
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>
|
</Suspense>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,7 @@ import React, { useEffect, useRef } from "react";
|
|||||||
const AdBlock = ({
|
const AdBlock = ({
|
||||||
className = "",
|
className = "",
|
||||||
adKey = "e0ca7c61c83457f093bbc2e261b43d31",
|
adKey = "e0ca7c61c83457f093bbc2e261b43d31",
|
||||||
adDomain = "www.highperformanceformat.com",
|
adDomain = "downconvenientmagnetic.com",
|
||||||
}) => {
|
}) => {
|
||||||
const iframeRef = useRef(null);
|
const iframeRef = useRef(null);
|
||||||
|
|
||||||
|
|||||||
34
src/components/CodeBlockComponent.js
Executable file
34
src/components/CodeBlockComponent.js
Executable 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;
|
||||||
107
src/components/FullscreenAdBanner.js
Executable file
107
src/components/FullscreenAdBanner.js
Executable 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;
|
||||||
@@ -1,25 +1,37 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from "react-router-dom";
|
||||||
import ToolSidebar from './ToolSidebar';
|
import ToolSidebar from "./ToolSidebar";
|
||||||
import NavigationConfirmModal from './NavigationConfirmModal';
|
import NavigationConfirmModal from "./NavigationConfirmModal";
|
||||||
import useNavigationGuard from '../hooks/useNavigationGuard';
|
import useNavigationGuard from "../hooks/useNavigationGuard";
|
||||||
import { Menu, X, ChevronDown, Terminal, Sparkles, Home } from 'lucide-react';
|
import { Menu, X, ChevronDown, Terminal, Sparkles, Home } from "lucide-react";
|
||||||
import ThemeToggle from './ThemeToggle';
|
import ThemeToggle from "./ThemeToggle";
|
||||||
import SEOHead from './SEOHead';
|
import SEOHead from "./SEOHead";
|
||||||
import ConsentBanner from './ConsentBanner';
|
import ConsentBanner from "./ConsentBanner";
|
||||||
import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
|
import {
|
||||||
import { useAnalytics } from '../hooks/useAnalytics';
|
NON_TOOLS,
|
||||||
|
TOOLS,
|
||||||
|
SITE_CONFIG,
|
||||||
|
getCategoryConfig,
|
||||||
|
} from "../config/tools";
|
||||||
|
import { useAnalytics } from "../hooks/useAnalytics";
|
||||||
|
|
||||||
const Layout = ({ children }) => {
|
const Layout = ({ children }) => {
|
||||||
const location = useLocation();
|
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 [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
|
|
||||||
// Initialize analytics tracking
|
// Initialize analytics tracking
|
||||||
useAnalytics();
|
useAnalytics();
|
||||||
|
|
||||||
const isActive = (path) => {
|
const isActive = (path) => {
|
||||||
return location.pathname === path;
|
return location.pathname === path;
|
||||||
};
|
};
|
||||||
@@ -32,9 +44,9 @@ const Layout = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -45,33 +57,42 @@ const Layout = ({ children }) => {
|
|||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
// Check if we're on a tool page (not homepage)
|
// 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)
|
// Check if we're on invoice preview page (no sidebar needed)
|
||||||
const isInvoicePreviewPage = location.pathname === '/invoice-preview';
|
const isInvoicePreviewPage = location.pathname === "/invoice-preview";
|
||||||
|
|
||||||
return (
|
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">
|
<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 */}
|
{/* SEO Head Management */}
|
||||||
<SEOHead />
|
<SEOHead />
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
|
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
|
||||||
<div className={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">
|
<div className="flex justify-between items-center h-16">
|
||||||
<button onClick={() => navigateWithGuard('/')} className="flex items-center group">
|
<button
|
||||||
|
onClick={() => navigateWithGuard("/")}
|
||||||
|
className="flex items-center group"
|
||||||
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 rounded-lg blur opacity-20 group-hover:opacity-40 transition-opacity"></div>
|
<div className="absolute inset-0 rounded-lg blur opacity-20 group-hover:opacity-40 transition-opacity"></div>
|
||||||
<div className="relative p-2">
|
<div className="relative p-2">
|
||||||
<img
|
<img
|
||||||
src="/logo.svg"
|
src="/logo.svg"
|
||||||
alt={SITE_CONFIG.title}
|
alt={SITE_CONFIG.title}
|
||||||
className="h-8 w-auto"
|
className="h-8 w-auto"
|
||||||
style={{ maxWidth: '150px' }}
|
style={{ maxWidth: "150px" }}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
// Fallback to Terminal icon with text if logo fails to load
|
// Fallback to Terminal icon with text if logo fails to load
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = "none";
|
||||||
e.target.nextSibling.style.display = 'flex';
|
e.target.nextSibling.style.display = "flex";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="hidden items-center space-x-3">
|
<div className="hidden items-center space-x-3">
|
||||||
@@ -83,7 +104,7 @@ const Layout = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
{/* Desktop Navigation - only show on homepage */}
|
{/* Desktop Navigation - only show on homepage */}
|
||||||
{!isToolPage && (
|
{!isToolPage && (
|
||||||
@@ -91,18 +112,18 @@ const Layout = ({ children }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDropdownOpen(false);
|
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 ${
|
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 ${
|
||||||
isActive('/')
|
isActive("/")
|
||||||
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
|
? "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'
|
: "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" />
|
<Home className="h-4 w-4" />
|
||||||
<span>Home</span>
|
<span>Home</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Tools Dropdown */}
|
{/* Tools Dropdown */}
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
<button
|
<button
|
||||||
@@ -113,11 +134,13 @@ const Layout = ({ children }) => {
|
|||||||
>
|
>
|
||||||
<Sparkles className="h-4 w-4" />
|
<Sparkles className="h-4 w-4" />
|
||||||
<span>Tools</span>
|
<span>Tools</span>
|
||||||
<ChevronDown className={`h-4 w-4 transition-transform duration-300 ${
|
<ChevronDown
|
||||||
isDropdownOpen ? 'rotate-180' : ''
|
className={`h-4 w-4 transition-transform duration-300 ${
|
||||||
}`} />
|
isDropdownOpen ? "rotate-180" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
{/* Dropdown Menu */}
|
||||||
{isDropdownOpen && (
|
{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">
|
<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">
|
<div className="relative">
|
||||||
{TOOLS.map((tool) => {
|
{TOOLS.map((tool) => {
|
||||||
const IconComponent = tool.icon;
|
const IconComponent = tool.icon;
|
||||||
const categoryConfig = getCategoryConfig(tool.category);
|
const categoryConfig = getCategoryConfig(
|
||||||
|
tool.category,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tool.path}
|
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 ${
|
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)
|
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'
|
? "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'
|
: "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" />
|
<IconComponent className="h-4 w-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="font-medium">{tool.name}</div>
|
<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>
|
||||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||||
<ChevronDown className="h-4 w-4 -rotate-90 text-slate-600" />
|
<ChevronDown className="h-4 w-4 -rotate-90 text-slate-600" />
|
||||||
@@ -159,9 +188,9 @@ const Layout = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
@@ -169,7 +198,11 @@ const Layout = ({ children }) => {
|
|||||||
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
|
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
|
||||||
aria-expanded={isMobileMenuOpen}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,49 +213,19 @@ const Layout = ({ children }) => {
|
|||||||
{isMobileMenuOpen && (
|
{isMobileMenuOpen && (
|
||||||
<>
|
<>
|
||||||
{/* Overlay */}
|
{/* Overlay */}
|
||||||
<div
|
<div
|
||||||
className="md:hidden fixed inset-0 bg-black/20 z-30"
|
className="md:hidden fixed inset-0 bg-black/20 z-30"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Menu */}
|
{/* 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="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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{/* Non-Tools Section */}
|
{/* Non-Tools Section */}
|
||||||
{NON_TOOLS.map((tool) => {
|
{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) => {
|
|
||||||
const IconComponent = tool.icon;
|
const IconComponent = tool.icon;
|
||||||
const categoryConfig = getCategoryConfig(tool.category);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tool.path}
|
key={tool.path}
|
||||||
@@ -230,31 +233,69 @@ const Layout = ({ children }) => {
|
|||||||
setIsMobileMenuOpen(false);
|
setIsMobileMenuOpen(false);
|
||||||
navigateWithGuard(tool.path);
|
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)
|
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'
|
? "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'
|
: "text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm`}>
|
<div
|
||||||
<IconComponent className="h-4 w-4 text-white" />
|
className={`p-2 rounded-lg ${isActive(tool.path) ? "bg-white/20" : "bg-gradient-to-br from-indigo-500 to-purple-500"} shadow-sm`}
|
||||||
</div>
|
>
|
||||||
<div className="flex-1">
|
<IconComponent
|
||||||
<div className="font-medium">{tool.name}</div>
|
className={`h-4 w-4 ${isActive(tool.path) ? "text-white" : "text-white"}`}
|
||||||
<div className="text-xs text-slate-600 dark:text-slate-600">{tool.description}</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<span>{tool.name}</span>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* 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 Content Area */}
|
||||||
<main className="flex-1 flex flex-col min-w-0 w-full max-w-full">
|
<main className="flex-1 flex flex-col min-w-0 w-full max-w-full">
|
||||||
{isToolPage && !isInvoicePreviewPage ? (
|
{isToolPage && !isInvoicePreviewPage ? (
|
||||||
@@ -263,22 +304,18 @@ const Layout = ({ children }) => {
|
|||||||
<ToolSidebar navigateWithGuard={navigateWithGuard} />
|
<ToolSidebar navigateWithGuard={navigateWithGuard} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex flex-col pl-0 lg:pl-16 min-w-0">
|
<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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : isInvoicePreviewPage ? (
|
) : isInvoicePreviewPage ? (
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<div className="flex-1">
|
<div className="flex-1">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<div className="flex-1">
|
<div className="flex-1">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
{/* Global Footer for Homepage */}
|
{/* 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">
|
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
<div className="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="relative">
|
||||||
<div className="absolute inset-0 rounded-lg blur opacity-20"></div>
|
<div className="absolute inset-0 rounded-lg blur opacity-20"></div>
|
||||||
<div className="relative p-2">
|
<div className="relative p-2">
|
||||||
<img
|
<img
|
||||||
src="/icon-192x192.png"
|
src="/icon-192x192.png"
|
||||||
alt={SITE_CONFIG.title}
|
alt={SITE_CONFIG.title}
|
||||||
className="h-16 w-auto"
|
className="h-16 w-auto"
|
||||||
style={{ maxWidth: '100px' }}
|
style={{ maxWidth: "100px" }}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
// Fallback to Terminal icon with text if logo fails to load
|
// Fallback to Terminal icon with text if logo fails to load
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = "none";
|
||||||
e.target.nextSibling.style.display = 'flex';
|
e.target.nextSibling.style.display = "flex";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="hidden items-center gap-3">
|
<div className="hidden items-center gap-3">
|
||||||
@@ -333,22 +370,26 @@ const Layout = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 text-xs">
|
<div className="flex items-center gap-4 text-xs">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigateWithGuard('/release-notes')}
|
onClick={() => navigateWithGuard("/release-notes")}
|
||||||
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||||
>
|
>
|
||||||
Release Notes
|
Release Notes
|
||||||
</button>
|
</button>
|
||||||
<span className="text-slate-300 dark:text-slate-600">•</span>
|
<span className="text-slate-300 dark:text-slate-600">
|
||||||
<button
|
•
|
||||||
onClick={() => navigateWithGuard('/privacy')}
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => navigateWithGuard("/privacy")}
|
||||||
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||||
>
|
>
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</button>
|
</button>
|
||||||
<span className="text-slate-300 dark:text-slate-600">•</span>
|
<span className="text-slate-300 dark:text-slate-600">
|
||||||
<button
|
•
|
||||||
onClick={() => navigateWithGuard('/terms')}
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => navigateWithGuard("/terms")}
|
||||||
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||||
>
|
>
|
||||||
Terms of Service
|
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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="flex items-center justify-center gap-2 mb-2">
|
<div className="flex items-center justify-center gap-2 mb-2">
|
||||||
<img
|
<img
|
||||||
src="/icon-192x192.png"
|
src="/icon-192x192.png"
|
||||||
alt={SITE_CONFIG.title}
|
alt={SITE_CONFIG.title}
|
||||||
className="h-16 w-auto"
|
className="h-16 w-auto"
|
||||||
style={{ maxWidth: '100px' }}
|
style={{ maxWidth: "100px" }}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
// Fallback to Terminal icon with text if logo fails to load
|
// Fallback to Terminal icon with text if logo fails to load
|
||||||
e.target.style.display = 'none';
|
e.target.style.display = "none";
|
||||||
e.target.nextSibling.style.display = 'flex';
|
e.target.nextSibling.style.display = "flex";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-4 text-xs">
|
<div className="flex items-center justify-center gap-4 text-xs">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigateWithGuard('/release-notes')}
|
onClick={() => navigateWithGuard("/release-notes")}
|
||||||
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||||
>
|
>
|
||||||
Release Notes
|
Release Notes
|
||||||
</button>
|
</button>
|
||||||
<span className="text-slate-300 dark:text-slate-600">•</span>
|
<span className="text-slate-300 dark:text-slate-600">•</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigateWithGuard('/privacy')}
|
onClick={() => navigateWithGuard("/privacy")}
|
||||||
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||||
>
|
>
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</button>
|
</button>
|
||||||
<span className="text-slate-300 dark:text-slate-600">•</span>
|
<span className="text-slate-300 dark:text-slate-600">•</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigateWithGuard('/terms')}
|
onClick={() => navigateWithGuard("/terms")}
|
||||||
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
|
||||||
>
|
>
|
||||||
Terms of Service
|
Terms of Service
|
||||||
@@ -414,7 +455,7 @@ const Layout = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* GDPR Consent Banner */}
|
{/* GDPR Consent Banner */}
|
||||||
<ConsentBanner />
|
<ConsentBanner />
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { X } from "lucide-react";
|
|
||||||
|
|
||||||
const MobileAdBanner = () => {
|
const MobileAdBanner = () => {
|
||||||
const [visible, setVisible] = useState(true);
|
|
||||||
const [closed, setClosed] = useState(false);
|
|
||||||
const iframeRef = useRef(null);
|
const iframeRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const wasClosed = sessionStorage.getItem("mobileAdClosed");
|
if (!iframeRef.current) return;
|
||||||
if (wasClosed === "true") {
|
|
||||||
setClosed(true);
|
|
||||||
setVisible(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!visible || closed || !iframeRef.current) return;
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (!iframeRef.current) return;
|
if (!iframeRef.current) return;
|
||||||
@@ -29,7 +18,7 @@ const MobileAdBanner = () => {
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<style>
|
<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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -42,7 +31,7 @@ const MobileAdBanner = () => {
|
|||||||
'params' : {}
|
'params' : {}
|
||||||
};
|
};
|
||||||
</script>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`);
|
||||||
@@ -50,33 +39,21 @@ const MobileAdBanner = () => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [visible, closed]);
|
}, []);
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setVisible(false);
|
|
||||||
setClosed(true);
|
|
||||||
sessionStorage.setItem("mobileAdClosed", "true");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!visible || closed) return null;
|
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
||||||
<button
|
<iframe
|
||||||
onClick={handleClose}
|
ref={iframeRef}
|
||||||
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"
|
style={{
|
||||||
aria-label="Close ad"
|
width: "320px",
|
||||||
>
|
height: "50px",
|
||||||
<X className="h-4 w-4" />
|
border: "none",
|
||||||
</button>
|
display: "block",
|
||||||
<div className="flex justify-center items-center py-2">
|
}}
|
||||||
<iframe
|
title="Mobile Advertisement"
|
||||||
ref={iframeRef}
|
sandbox="allow-scripts allow-same-origin"
|
||||||
style={{ width: "320px", height: "50px", border: "none" }}
|
/>
|
||||||
title="Mobile Advertisement"
|
|
||||||
sandbox="allow-scripts allow-same-origin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
289
src/components/RichMarkdownEditor.js
Executable file
289
src/components/RichMarkdownEditor.js
Executable 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
@@ -13,7 +13,7 @@ const TabletAdSection = () => {
|
|||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<AdBlock
|
<AdBlock
|
||||||
adKey="7c55aebcdd74f6e9a8dc24bd13e7d949"
|
adKey="7c55aebcdd74f6e9a8dc24bd13e7d949"
|
||||||
adDomain="www.highperformanceformat.com"
|
adDomain="downconvenientmagnetic.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
|
|||||||
@@ -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
|
// Master tools configuration - single source of truth
|
||||||
export const TOOL_CATEGORIES = {
|
export const TOOL_CATEGORIES = {
|
||||||
navigation: {
|
navigation: {
|
||||||
name: 'Navigation',
|
name: "Navigation",
|
||||||
color: 'from-slate-500 to-slate-600',
|
color: "from-slate-500 to-slate-600",
|
||||||
hoverColor: 'slate-600',
|
hoverColor: "slate-600",
|
||||||
textColor: 'text-slate-600',
|
textColor: "text-slate-600",
|
||||||
hoverTextColor: 'hover:text-slate-700 dark:hover:text-slate-600'
|
hoverTextColor: "hover:text-slate-700 dark:hover:text-slate-600",
|
||||||
},
|
},
|
||||||
editor: {
|
editor: {
|
||||||
name: 'Editor',
|
name: "Editor",
|
||||||
color: 'from-blue-500 to-cyan-500',
|
color: "from-blue-500 to-cyan-500",
|
||||||
hoverColor: 'blue-600',
|
hoverColor: "blue-600",
|
||||||
textColor: 'text-blue-600',
|
textColor: "text-blue-600",
|
||||||
hoverTextColor: 'hover:text-blue-700 dark:hover:text-blue-400'
|
hoverTextColor: "hover:text-blue-700 dark:hover:text-blue-400",
|
||||||
},
|
},
|
||||||
encoder: {
|
encoder: {
|
||||||
name: 'Encoder',
|
name: "Encoder",
|
||||||
color: 'from-purple-500 to-pink-500',
|
color: "from-purple-500 to-pink-500",
|
||||||
hoverColor: 'purple-600',
|
hoverColor: "purple-600",
|
||||||
textColor: 'text-purple-600',
|
textColor: "text-purple-600",
|
||||||
hoverTextColor: 'hover:text-purple-700 dark:hover:text-purple-400'
|
hoverTextColor: "hover:text-purple-700 dark:hover:text-purple-400",
|
||||||
},
|
},
|
||||||
formatter: {
|
formatter: {
|
||||||
name: 'Formatter',
|
name: "Formatter",
|
||||||
color: 'from-green-500 to-emerald-500',
|
color: "from-green-500 to-emerald-500",
|
||||||
hoverColor: 'green-600',
|
hoverColor: "green-600",
|
||||||
textColor: 'text-green-600',
|
textColor: "text-green-600",
|
||||||
hoverTextColor: 'hover:text-green-700 dark:hover:text-green-400'
|
hoverTextColor: "hover:text-green-700 dark:hover:text-green-400",
|
||||||
},
|
},
|
||||||
analyzer: {
|
analyzer: {
|
||||||
name: 'Analyzer',
|
name: "Analyzer",
|
||||||
color: 'from-orange-500 to-red-500',
|
color: "from-orange-500 to-red-500",
|
||||||
hoverColor: 'orange-600',
|
hoverColor: "orange-600",
|
||||||
textColor: 'text-orange-600',
|
textColor: "text-orange-600",
|
||||||
hoverTextColor: 'hover:text-orange-700 dark:hover:text-orange-400'
|
hoverTextColor: "hover:text-orange-700 dark:hover:text-orange-400",
|
||||||
},
|
},
|
||||||
non_tools: {
|
non_tools: {
|
||||||
name: 'Site Navigation',
|
name: "Site Navigation",
|
||||||
color: 'from-indigo-500 to-purple-500',
|
color: "from-indigo-500 to-purple-500",
|
||||||
hoverColor: 'indigo-600',
|
hoverColor: "indigo-600",
|
||||||
textColor: 'text-indigo-600',
|
textColor: "text-indigo-600",
|
||||||
hoverTextColor: 'hover:text-indigo-700 dark:hover:text-indigo-400'
|
hoverTextColor: "hover:text-indigo-700 dark:hover:text-indigo-400",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TOOLS = [
|
export const TOOLS = [
|
||||||
{
|
{
|
||||||
path: '/object-editor',
|
path: "/object-editor",
|
||||||
name: 'Object Editor',
|
name: "Object Editor",
|
||||||
icon: Edit3,
|
icon: Edit3,
|
||||||
description: 'Visual editor for JSON and PHP serialized objects with mindmap visualization',
|
description:
|
||||||
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor'],
|
"Visual editor for JSON and PHP serialized objects with mindmap visualization",
|
||||||
category: 'editor'
|
tags: ["Visual", "JSON", "PHP", "Objects", "Editor"],
|
||||||
|
category: "editor",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/table-editor',
|
path: "/table-editor",
|
||||||
name: 'Table Editor',
|
name: "Table Editor",
|
||||||
icon: Table,
|
icon: Table,
|
||||||
description: 'Import, edit, and export tabular data from URLs, files, or paste CSV/JSON',
|
description:
|
||||||
tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor'],
|
"Import, edit, and export tabular data from URLs, files, or paste CSV/JSON",
|
||||||
category: 'editor'
|
tags: ["Table", "CSV", "JSON", "Data", "Editor"],
|
||||||
|
category: "editor",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/markdown-editor',
|
path: "/markdown-editor",
|
||||||
name: 'Markdown Editor',
|
name: "Markdown Editor",
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
description: 'Write and preview markdown with live rendering, syntax highlighting, and export options',
|
description:
|
||||||
tags: ['Markdown', 'Editor', 'Preview', 'Export', 'GFM'],
|
"Write and preview markdown with live rendering, syntax highlighting, and export options",
|
||||||
category: 'editor'
|
tags: ["Markdown", "Editor", "Preview", "Export", "GFM"],
|
||||||
|
category: "editor",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/invoice-editor',
|
path: "/diagram-editor",
|
||||||
name: 'Invoice 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,
|
icon: FileText,
|
||||||
description: 'Create, edit, and export professional invoices with PDF generation',
|
description:
|
||||||
tags: ['Invoice', 'PDF', 'Business', 'Billing', 'Export'],
|
"Create, edit, and export professional invoices with PDF generation",
|
||||||
category: 'editor'
|
tags: ["Invoice", "PDF", "Business", "Billing", "Export"],
|
||||||
|
category: "editor",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/url',
|
path: "/url",
|
||||||
name: 'URL Encoder/Decoder',
|
name: "URL Encoder/Decoder",
|
||||||
icon: LinkIcon,
|
icon: LinkIcon,
|
||||||
description: 'Encode and decode URLs and query parameters',
|
description: "Encode and decode URLs and query parameters",
|
||||||
tags: ['URL', 'Encode', 'Decode'],
|
tags: ["URL", "Encode", "Decode"],
|
||||||
category: 'encoder'
|
category: "encoder",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/base64',
|
path: "/base64",
|
||||||
name: 'Base64 Encoder/Decoder',
|
name: "Base64 Encoder/Decoder",
|
||||||
icon: Hash,
|
icon: Hash,
|
||||||
description: 'Convert text to Base64 and back with support for files',
|
description: "Convert text to Base64 and back with support for files",
|
||||||
tags: ['Base64', 'Encode', 'Binary'],
|
tags: ["Base64", "Encode", "Binary"],
|
||||||
category: 'encoder'
|
category: "encoder",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/beautifier',
|
path: "/beautifier",
|
||||||
name: 'Code Beautifier/Minifier',
|
name: "Code Beautifier/Minifier",
|
||||||
icon: Wand2,
|
icon: Wand2,
|
||||||
description: 'Format and minify JSON, XML, SQL, CSS, and HTML code',
|
description: "Format and minify JSON, XML, SQL, CSS, and HTML code",
|
||||||
tags: ['Format', 'Minify', 'Beautify'],
|
tags: ["Format", "Minify", "Beautify"],
|
||||||
category: 'formatter'
|
category: "formatter",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/diff',
|
path: "/diff",
|
||||||
name: 'Text Diff Checker',
|
name: "Text Diff Checker",
|
||||||
icon: GitCompare,
|
icon: GitCompare,
|
||||||
description: 'Compare two texts and highlight differences line by line',
|
description: "Compare two texts and highlight differences line by line",
|
||||||
tags: ['Diff', 'Compare', 'Text'],
|
tags: ["Diff", "Compare", "Text"],
|
||||||
category: 'analyzer'
|
category: "analyzer",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/text-length',
|
path: "/text-length",
|
||||||
name: 'Text Length Checker',
|
name: "Text Length Checker",
|
||||||
icon: Type,
|
icon: Type,
|
||||||
description: 'Analyze text length, word count, and other text statistics',
|
description: "Analyze text length, word count, and other text statistics",
|
||||||
tags: ['Text', 'Length', 'Statistics'],
|
tags: ["Text", "Length", "Statistics"],
|
||||||
category: 'analyzer'
|
category: "analyzer",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Non-tool navigation items (homepage, what's new, etc.)
|
// Non-tool navigation items (homepage, what's new, etc.)
|
||||||
export const NON_TOOLS = [
|
export const NON_TOOLS = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: "/",
|
||||||
name: 'Home',
|
name: "Home",
|
||||||
icon: Home,
|
icon: Home,
|
||||||
description: 'Back to homepage',
|
description: "Back to homepage",
|
||||||
category: 'non_tools'
|
category: "non_tools",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Navigation tools (for sidebar) - combines non-tools and tools
|
// Navigation tools (for sidebar) - combines non-tools and tools
|
||||||
export const NAVIGATION_TOOLS = [
|
export const NAVIGATION_TOOLS = [...NON_TOOLS, ...TOOLS];
|
||||||
...NON_TOOLS,
|
|
||||||
...TOOLS
|
|
||||||
];
|
|
||||||
|
|
||||||
// Site configuration
|
// Site configuration
|
||||||
export const SITE_CONFIG = {
|
export const SITE_CONFIG = {
|
||||||
domain: 'https://dewe.dev',
|
domain: "https://dewe.dev",
|
||||||
title: 'Dewe.Dev',
|
title: "Dewe.Dev",
|
||||||
subtitle: 'Professional Developer Utilities',
|
subtitle: "Professional Developer Utilities",
|
||||||
slogan: 'Code faster, debug smarter, ship better',
|
slogan: "Code faster, debug smarter, ship better",
|
||||||
description: 'Professional-grade utilities for modern developers',
|
description: "Professional-grade utilities for modern developers",
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
totalTools: TOOLS.length
|
totalTools: TOOLS.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper functions
|
// 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 = () => {
|
export const getCategoryStats = () => {
|
||||||
const stats = {};
|
const stats = {};
|
||||||
Object.keys(TOOL_CATEGORIES).forEach(key => {
|
Object.keys(TOOL_CATEGORIES).forEach((key) => {
|
||||||
if (key !== 'navigation') {
|
if (key !== "navigation") {
|
||||||
stats[key] = getToolsByCategory(key).length;
|
stats[key] = getToolsByCategory(key).length;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
126
src/index.css
126
src/index.css
@@ -2,69 +2,75 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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 {
|
@layer base {
|
||||||
html {
|
html {
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family:
|
||||||
overflow-x: hidden;
|
system-ui,
|
||||||
width: 100%;
|
-apple-system,
|
||||||
max-width: 100vw;
|
BlinkMacSystemFont,
|
||||||
}
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
body {
|
sans-serif;
|
||||||
overflow-x: hidden;
|
width: 100%;
|
||||||
width: 100%;
|
max-width: 100vw;
|
||||||
max-width: 100vw;
|
}
|
||||||
}
|
|
||||||
|
body {
|
||||||
#root {
|
width: 100%;
|
||||||
overflow-x: hidden;
|
max-width: 100vw;
|
||||||
width: 100%;
|
}
|
||||||
max-width: 100vw;
|
|
||||||
min-width: 0;
|
#root {
|
||||||
}
|
width: 100%;
|
||||||
|
max-width: 100vw;
|
||||||
code, pre {
|
min-width: 0;
|
||||||
font-family: 'JetBrains Mono', Monaco, 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', monospace;
|
}
|
||||||
}
|
|
||||||
|
code,
|
||||||
|
pre {
|
||||||
|
font-family:
|
||||||
|
"JetBrains Mono", Monaco, "Cascadia Code", "Segoe UI Mono",
|
||||||
|
"Roboto Mono", monospace;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.tool-card {
|
.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;
|
@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 {
|
.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;
|
@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 {
|
.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;
|
@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 {
|
.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;
|
@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 {
|
.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;
|
@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 {
|
.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;
|
@apply p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors duration-200 text-gray-700 dark:text-gray-300 font-medium text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-button {
|
.copy-button {
|
||||||
@apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200;
|
@apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-hide {
|
.scrollbar-hide {
|
||||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollbar-hide::-webkit-scrollbar {
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
display: none; /* Safari and Chrome */
|
display: none; /* Safari and Chrome */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
586
src/pages/DiagramEditor.js
Executable file
586
src/pages/DiagramEditor.js
Executable 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
@@ -1,23 +1,25 @@
|
|||||||
/* GitHub-style Markdown Preview Styling */
|
/* GitHub-style Markdown Preview Styling */
|
||||||
.markdown-preview {
|
.markdown-preview {
|
||||||
color: #24292f;
|
color: #24292f;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
|
font-family:
|
||||||
font-size: 16px;
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica,
|
||||||
line-height: 1.6;
|
Arial, sans-serif;
|
||||||
word-wrap: break-word;
|
font-size: 16px;
|
||||||
overflow-wrap: break-word;
|
line-height: 1.6;
|
||||||
max-width: 100%;
|
word-wrap: break-word;
|
||||||
word-break: break-word;
|
overflow-wrap: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure all child elements respect container width */
|
/* Ensure all child elements respect container width */
|
||||||
.markdown-preview * {
|
.markdown-preview * {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview {
|
.dark .markdown-preview {
|
||||||
color: #c9d1d9;
|
color: #c9d1d9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h1,
|
.markdown-preview h1,
|
||||||
@@ -26,254 +28,256 @@
|
|||||||
.markdown-preview h4,
|
.markdown-preview h4,
|
||||||
.markdown-preview h5,
|
.markdown-preview h5,
|
||||||
.markdown-preview h6 {
|
.markdown-preview h6 {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h1 {
|
.markdown-preview h1 {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
border-bottom: 1px solid #d0d7de;
|
border-bottom: 1px solid #d0d7de;
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview h1 {
|
.dark .markdown-preview h1 {
|
||||||
border-bottom-color: #21262d;
|
border-bottom-color: #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h2 {
|
.markdown-preview h2 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
border-bottom: 1px solid #d0d7de;
|
border-bottom: 1px solid #d0d7de;
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview h2 {
|
.dark .markdown-preview h2 {
|
||||||
border-bottom-color: #21262d;
|
border-bottom-color: #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h3 {
|
.markdown-preview h3 {
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h4 {
|
.markdown-preview h4 {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h5 {
|
.markdown-preview h5 {
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview h6 {
|
.markdown-preview h6 {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
color: #57606a;
|
color: #57606a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview h6 {
|
.dark .markdown-preview h6 {
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview p {
|
.markdown-preview p {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inline code - with background */
|
/* Inline code - with background */
|
||||||
.markdown-preview code {
|
.markdown-preview code {
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
background-color: rgba(175, 184, 193, 0.2);
|
background-color: rgba(175, 184, 193, 0.2);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
font-family:
|
||||||
|
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||||
|
"Liberation Mono", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview code {
|
.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 */
|
/* Code block wrapper with header */
|
||||||
.markdown-preview .code-block-wrapper {
|
.markdown-preview .code-block-wrapper {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #d0d7de;
|
border: 1px solid #d0d7de;
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview .code-block-wrapper {
|
.dark .markdown-preview .code-block-wrapper {
|
||||||
border-color: #30363d;
|
border-color: #30363d;
|
||||||
background-color: #0d1117;
|
background-color: #0d1117;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code block header */
|
/* Code block header */
|
||||||
.markdown-preview .code-block-header {
|
.markdown-preview .code-block-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fa;
|
||||||
border-bottom: 1px solid #d0d7de;
|
border-bottom: 1px solid #d0d7de;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview .code-block-header {
|
.dark .markdown-preview .code-block-header {
|
||||||
background-color: #161b22;
|
background-color: #161b22;
|
||||||
border-bottom-color: #30363d;
|
border-bottom-color: #30363d;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Language label */
|
/* Language label */
|
||||||
.markdown-preview .code-block-language {
|
.markdown-preview .code-block-language {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #57606a;
|
color: #57606a;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview .code-block-language {
|
.dark .markdown-preview .code-block-language {
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Copy button */
|
/* Copy button */
|
||||||
.markdown-preview .code-block-copy {
|
.markdown-preview .code-block-copy {
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid #d0d7de;
|
border: 1px solid #d0d7de;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #24292f;
|
color: #24292f;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview .code-block-copy:hover {
|
.markdown-preview .code-block-copy:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
border-color: #1f2328;
|
border-color: #1f2328;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview .code-block-copy {
|
.dark .markdown-preview .code-block-copy {
|
||||||
color: #c9d1d9;
|
color: #c9d1d9;
|
||||||
border-color: #30363d;
|
border-color: #30363d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview .code-block-copy:hover {
|
.dark .markdown-preview .code-block-copy:hover {
|
||||||
background-color: #21262d;
|
background-color: #21262d;
|
||||||
border-color: #8b949e;
|
border-color: #8b949e;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code blocks - with background */
|
/* Code blocks - with background */
|
||||||
.markdown-preview .code-block-wrapper pre {
|
.markdown-preview .code-block-wrapper pre {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
background-color: #0d1117;
|
background-color: #0d1117;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Legacy pre blocks (without wrapper) */
|
/* Legacy pre blocks (without wrapper) */
|
||||||
.markdown-preview pre:not(.code-block-wrapper pre) {
|
.markdown-preview pre:not(.code-block-wrapper pre) {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
background-color: #afb8c133;
|
background-color: #afb8c133;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview pre:not(.code-block-wrapper pre) {
|
.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) */
|
/* Code inside pre blocks - NO background (transparent) */
|
||||||
.markdown-preview pre code {
|
.markdown-preview pre code {
|
||||||
display: inline;
|
display: inline;
|
||||||
max-width: auto;
|
max-width: auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Preserve highlight.js syntax highlighting colors */
|
/* Preserve highlight.js syntax highlighting colors */
|
||||||
.markdown-preview pre code.hljs {
|
.markdown-preview pre code.hljs {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview table {
|
.markdown-preview table {
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview table tr {
|
.markdown-preview table tr {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-top: 1px solid #d0d7de;
|
border-top: 1px solid #d0d7de;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview table tr {
|
.dark .markdown-preview table tr {
|
||||||
background-color: #0d1117;
|
background-color: #0d1117;
|
||||||
border-top-color: #21262d;
|
border-top-color: #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview table tr:nth-child(2n) {
|
.markdown-preview table tr:nth-child(2n) {
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview table tr:nth-child(2n) {
|
.dark .markdown-preview table tr:nth-child(2n) {
|
||||||
background-color: #161b22;
|
background-color: #161b22;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview table th,
|
.markdown-preview table th,
|
||||||
.markdown-preview table td {
|
.markdown-preview table td {
|
||||||
padding: 6px 13px;
|
padding: 6px 13px;
|
||||||
border: 1px solid #d0d7de;
|
border: 1px solid #d0d7de;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview table th,
|
.dark .markdown-preview table th,
|
||||||
.dark .markdown-preview table td {
|
.dark .markdown-preview table td {
|
||||||
border-color: #21262d;
|
border-color: #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview table th {
|
.markdown-preview table th {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview table th {
|
.dark .markdown-preview table th {
|
||||||
background-color: #161b22;
|
background-color: #161b22;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview blockquote {
|
.markdown-preview blockquote {
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
color: #57606a;
|
color: #57606a;
|
||||||
border-left: 0.25em solid #d0d7de;
|
border-left: 0.25em solid #d0d7de;
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview blockquote {
|
.dark .markdown-preview blockquote {
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
border-left-color: #3b434b;
|
border-left-color: #3b434b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ul,
|
.markdown-preview ul,
|
||||||
.markdown-preview ol {
|
.markdown-preview ol {
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nested lists */
|
/* Nested lists */
|
||||||
@@ -281,100 +285,192 @@
|
|||||||
.markdown-preview ul ol,
|
.markdown-preview ul ol,
|
||||||
.markdown-preview ol ul,
|
.markdown-preview ol ul,
|
||||||
.markdown-preview ol ol {
|
.markdown-preview ol ol {
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
margin-bottom: 0.25em;
|
margin-bottom: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List items */
|
/* List items */
|
||||||
.markdown-preview li {
|
.markdown-preview li {
|
||||||
margin-bottom: 0.25em;
|
margin-bottom: 0.25em;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview li + li {
|
.markdown-preview li + li {
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Better bullet points */
|
/* Better bullet points */
|
||||||
.markdown-preview ul > li {
|
.markdown-preview ul > li {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ul ul > li {
|
.markdown-preview ul ul > li {
|
||||||
list-style-type: circle;
|
list-style-type: circle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ul ul ul > li {
|
.markdown-preview ul ul ul > li {
|
||||||
list-style-type: square;
|
list-style-type: square;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ordered list styling */
|
/* Ordered list styling */
|
||||||
.markdown-preview ol > li {
|
.markdown-preview ol > li {
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ol ol > li {
|
.markdown-preview ol ol > li {
|
||||||
list-style-type: lower-alpha;
|
list-style-type: lower-alpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview ol ol ol > li {
|
.markdown-preview ol ol ol > li {
|
||||||
list-style-type: lower-roman;
|
list-style-type: lower-roman;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List item content spacing */
|
/* List item content spacing */
|
||||||
.markdown-preview li > p {
|
.markdown-preview li > p {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview li > p:first-child {
|
.markdown-preview li > p:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview li > p:last-child {
|
.markdown-preview li > p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview hr {
|
.markdown-preview hr {
|
||||||
height: 0.25em;
|
height: 0.25em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 24px 0;
|
margin: 24px 0;
|
||||||
background-color: #d0d7de;
|
background-color: #d0d7de;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview hr {
|
.dark .markdown-preview hr {
|
||||||
background-color: #21262d;
|
background-color: #21262d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview a {
|
.markdown-preview a {
|
||||||
color: #0969da;
|
color: #0969da;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .markdown-preview a {
|
.dark .markdown-preview a {
|
||||||
color: #58a6ff;
|
color: #58a6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview a:hover {
|
.markdown-preview a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview strong {
|
.markdown-preview strong {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview em {
|
.markdown-preview em {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview u {
|
.markdown-preview u {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview img {
|
.markdown-preview img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin: 16px 0;
|
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) */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,165 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||||
"./src/**/*.{js,jsx,ts,tsx}",
|
darkMode: "class", // Enable manual dark mode control via class
|
||||||
],
|
|
||||||
darkMode: 'class', // Enable manual dark mode control via class
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
50: '#f0f9ff',
|
50: "#f0f9ff",
|
||||||
100: '#e0f2fe',
|
100: "#e0f2fe",
|
||||||
200: '#bae6fd',
|
200: "#bae6fd",
|
||||||
300: '#7dd3fc',
|
300: "#7dd3fc",
|
||||||
400: '#38bdf8',
|
400: "#38bdf8",
|
||||||
500: '#0ea5e9',
|
500: "#0ea5e9",
|
||||||
600: '#0284c7',
|
600: "#0284c7",
|
||||||
700: '#0369a1',
|
700: "#0369a1",
|
||||||
800: '#075985',
|
800: "#075985",
|
||||||
900: '#0c4a6e',
|
900: "#0c4a6e",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
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: {
|
typography: (theme) => ({
|
||||||
'1/4': '25%',
|
DEFAULT: {
|
||||||
'1/2': '50%',
|
css: {
|
||||||
'3/4': '75%',
|
"--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")],
|
||||||
}
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user