Compare commits

...

17 Commits

Author SHA1 Message Date
Dwindi Ramadhana
1047642909 fix(diagram): correct flowchart template edge connections to match diagram logic 2026-06-14 20:06:01 +07:00
Dwindi Ramadhana
8caf6fbba5 fix(diagram): add missing Copy icon import for DiagramEditor export section 2026-06-14 19:57:25 +07:00
Dwindi Ramadhana
3727ace366 fix(build): remove unused imports in DiagramEditor and ReactFlowEditor 2026-06-14 18:45:14 +07:00
Dwindi Ramadhana
81c399ab42 fix(build): add missing DiagramEditor and FullscreenAdBanner files to git tracking 2026-06-14 16:05:40 +07:00
Dwindi Ramadhana
518b0127d2 fix(diagram): resolve DOMURL undefined error causing build failure 2026-06-14 15:55:08 +07:00
Dwindi Ramadhana
deb2bf0b8a fix(ads): adjust internal iframe styles to minimize bottom gap 2026-06-14 15:43:42 +07:00
Dwindi Ramadhana
e4ccff4bbf chore: update TODO.md to mark Markdown Editor MVP tasks as completed 2026-06-14 13:30:37 +07:00
Dwindi Ramadhana
0b1cfbdabd fix(layout): remove global overflow-x-hidden to restore sticky sidebar behavior 2026-06-14 13:15:51 +07:00
Dwindi Ramadhana
c580a5f7b0 fix(markdown-editor): align max width and formatting across read and edit views 2026-06-14 12:35:18 +07:00
Dwindi Ramadhana
9232052508 chore(ads): update adsterra anti-adblock custom domain to downconvenientmagnetic 2026-06-14 12:32:16 +07:00
Dwindi Ramadhana
fcbfeb44f8 fix(build): remove unused import EyeOff to pass strict CI build 2026-06-14 11:52:19 +07:00
Dwindi Ramadhana
dd0b98e077 chore: remove metadata files created by SSD 2026-06-14 01:01:03 +07:00
Dwindi Ramadhana
dcba58c2b9 chore: ignore OS meta files and update changelog for WYSIWYG and Object Editor updates 2026-06-14 00:56:38 +07:00
Dwindi Ramadhana
7b3dce06ea refactor(markdown-editor): migrate to tiptap for WYSIWYG editing, standardize UI spacing, and update export engine 2026-06-14 00:54:01 +07:00
Dwindi Ramadhana
13e694aa82 feat: add multidimensional search, preview mode prioritization, and collapse/expand all to Object Editor 2026-06-13 20:11:07 +07:00
Dwindi Ramadhana
6a14eebf25 chore: remove OfferBlock learn more button and change to coming soon 2026-06-13 18:45:52 +07:00
dwindown
3a475e9df2 feat: WCAG AA accessibility, code splitting, responsive ads layout
- Add React.lazy code splitting for all 15 tool pages
- Fix WCAG AA contrast issues (304 text color fixes)
- Add ARIA labels and aria-expanded to navigation buttons
- Add aria-live for error announcements in tools
- Implement responsive ad layout:
  - Desktop (≥1280px): Right sidebar with 3 ad units
  - Tablet (1024-1279px): Bottom section with 3 horizontal units
  - Mobile (<1024px): Fixed bottom banner
- Add TabletAdSection component for tablet ad placement
- Integrate Onidel affiliate partnership
- Update all Adsterra domains to solutionbiologyisle.com
- Add release notes for 2026-02-18 updates
2026-02-18 18:57:31 +07:00
109 changed files with 39750 additions and 12607 deletions

0
.env Normal file → Executable file
View File

0
.env.example Normal file → Executable file
View File

3
.gitignore vendored Normal file → Executable file
View File

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

View File

@@ -1,13 +0,0 @@
---
trigger: always_on
---
keep look the issue globally, not narrow. we are done chasing symtomp with narrow sight, we have things to be achieved:
A. main goal: having a working HTML Preview with element inspector and editor feature, and
B. sub goal: implementing the "stable option A DOM Manipulation" properly to reach the main goal (A)
In every reported issue, check if that prevent us to achieved the sub goal. Failing sub goal means fail to reach the main goal. So pivot everything to make a success sub goal, to achieve main goal.
I believe promised sub goal is the way to get succeed on the main goal.
Avoid any looping thought

0
BACKEND_REQUIREMENTS.md Normal file → Executable file
View File

0
DOCUMENTATION_INDEX.md Normal file → Executable file
View File

0
EDITOR_CHECKLIST.md Normal file → Executable file
View File

0
EDITOR_TOOL_GUIDE.md Normal file → Executable file
View File

0
FEATURE_TOGGLE_GUIDE.md Normal file → Executable file
View File

811
PLAN_ACCOUNTS_AND_STORAGE.md Executable file
View File

@@ -0,0 +1,811 @@
# Dewe.Dev — Accounts, File Storage & Pro Features Plan
**Created:** February 18, 2026
---
## Executive Summary
Add user accounts and cloud file storage to dewe.dev without changing the existing UX. Visitors and logged-in users use the exact same tools. The only additions: a login/account page, save/load buttons per tool, and server-side URL fetching for Pro users. Backend powered by self-hosted Supabase on Coolify.
---
## Guiding Principles
1. **Zero UX disruption** — Tools work identically for everyone. Auth is invisible until the user wants it.
2. **Frontend stays king** — All processing remains client-side. The backend is only for storage, auth, and proxy.
3. **Progressive enhancement** — Each feature layer (auth → storage → proxy → billing) can ship independently.
4. **Privacy preserved** — File content is user-owned. RLS enforces isolation. No analytics on file content.
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────────┐
│ dewe.dev (React SPA) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────────┐ │
│ │ MD │ │Invoice │ │ Table │ │ Object │ │
│ │ Editor │ │ Editor │ │ Editor │ │ Editor │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └─────┬─────┘ │
│ │ │ │ │ │
│ └────────────┴─────┬──────┴──────────────┘ │
│ │ │
│ ┌───────────┴───────────┐ │
│ │ useAuth() context │ │
│ │ useUserFiles() hook │ │
│ └───────────┬───────────┘ │
└──────────────────────────┼──────────────────────────────┘
HTTPS (Supabase JS SDK)
┌──────────────────────────┼──────────────────────────────┐
│ Self-hosted Supabase (Coolify) │
│ │ │
│ ┌───────────┐ ┌────────┴────────┐ ┌───────────────┐ │
│ │ GoTrue │ │ PostgREST │ │ Edge Functions│ │
│ │ (Auth) │ │ (REST API) │ │ (CORS Proxy) │ │
│ └───────────┘ └────────┬────────┘ └───────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ PostgreSQL │ │
│ │ │ │
│ │ • auth.users │ │
│ │ • profiles │ │
│ │ • user_files │ │
│ │ • proxy_logs │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## Phase 1: Supabase Setup on Coolify
**Goal:** Get the backend running and verified before writing any frontend code.
### 1.1 Deploy Supabase on Coolify
Coolify supports Supabase as a one-click service (or via Docker Compose).
**Resource Requirements:**
| Resource | Minimum | Recommended |
|----------|---------|-------------|
| CPU | 2 cores | 4 cores |
| RAM | 4 GB | 8 GB |
| Disk | 20 GB SSD | 50 GB SSD |
**Services that spin up:**
- PostgreSQL (database)
- GoTrue (auth)
- PostgREST (auto-generated REST API)
- Realtime (websockets — can disable if unused)
- Storage API (file storage — optional for this use case)
- Edge Runtime (Deno functions)
- Studio (admin dashboard)
- Kong/Envoy (API gateway)
**Critical Coolify configuration:**
- Map persistent volumes for PostgreSQL data directory — data survives container restarts
- Set up SSL via Coolify's built-in Let's Encrypt
- Configure `SITE_URL` to `https://dewe.dev`
- Set `ADDITIONAL_REDIRECT_URLS` for localhost dev
- Expose only the API gateway port (default 8000) and Studio port (default 3000)
### 1.2 Database Schema
```sql
-- Profiles table (extends auth.users)
create table profiles (
id uuid primary key references auth.users on delete cascade,
display_name text,
tier text not null default 'free' check (tier in ('free', 'pro')),
tier_expires_at timestamptz,
storage_used_bytes bigint default 0,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Auto-create profile on signup
create or replace function handle_new_user()
returns trigger as $$
begin
insert into profiles (id, display_name)
values (new.id, coalesce(new.raw_user_meta_data->>'display_name', split_part(new.email, '@', 1)));
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute function handle_new_user();
-- Sync tier to JWT custom claims (app_metadata)
create or replace function sync_tier_to_claims()
returns trigger as $$
begin
update auth.users
set raw_app_meta_data = raw_app_meta_data || jsonb_build_object('tier', new.tier)
where id = new.id;
return new;
end;
$$ language plpgsql security definer;
create trigger on_tier_change
after update of tier on profiles
for each row execute function sync_tier_to_claims();
-- User files table
create table user_files (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users on delete cascade not null,
tool_type text not null, -- 'markdown', 'invoice', 'object', 'table', 'beautifier'
name text not null,
content jsonb not null, -- The actual file data
metadata jsonb default '{}', -- Optional: last export format, preview text, etc.
size_bytes integer generated always as (octet_length(content::text)) stored,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Indexes
create index idx_user_files_user_tool on user_files(user_id, tool_type);
create index idx_user_files_updated on user_files(user_id, updated_at desc);
-- Proxy usage logging (for rate limiting & analytics)
create table proxy_logs (
id bigserial primary key,
user_id uuid references auth.users not null,
target_url text not null,
method text not null,
status_code integer,
response_size integer,
duration_ms integer,
created_at timestamptz default now()
);
create index idx_proxy_logs_user_time on proxy_logs(user_id, created_at desc);
```
### 1.3 Row Level Security (RLS)
```sql
-- Profiles: users can read/update only their own
alter table profiles enable row level security;
create policy "Users can view own profile"
on profiles for select using (auth.uid() = id);
create policy "Users can update own profile"
on profiles for update using (auth.uid() = id);
-- User files: full CRUD, own files only
alter table user_files enable row level security;
create policy "Users can view own files"
on user_files for select using (auth.uid() = user_id);
create policy "Users can insert own files"
on user_files for insert with check (auth.uid() = user_id);
create policy "Users can update own files"
on user_files for update using (auth.uid() = user_id);
create policy "Users can delete own files"
on user_files for delete using (auth.uid() = user_id);
-- Proxy logs: users can view own logs
alter table proxy_logs enable row level security;
create policy "Users can view own proxy logs"
on proxy_logs for select using (auth.uid() = user_id);
```
### 1.4 Storage Limits
| Tier | Max files | Max file size | Total storage |
|------|-----------|---------------|---------------|
| Free | 20 | 256 KB | 5 MB |
| Pro | Unlimited | 2 MB | 100 MB |
Enforced via database function:
```sql
create or replace function check_storage_limit()
returns trigger as $$
declare
current_count integer;
current_size bigint;
user_tier text;
max_count integer;
max_total bigint;
begin
select tier into user_tier from profiles where id = new.user_id;
if user_tier = 'free' then
max_count := 20;
max_total := 5 * 1024 * 1024; -- 5 MB
else
max_count := 10000;
max_total := 100 * 1024 * 1024; -- 100 MB
end if;
select count(*), coalesce(sum(size_bytes), 0)
into current_count, current_size
from user_files where user_id = new.user_id;
if current_count >= max_count then
raise exception 'File limit reached (% files for % tier)', max_count, user_tier;
end if;
if current_size + octet_length(new.content::text) > max_total then
raise exception 'Storage limit reached (% bytes for % tier)', max_total, user_tier;
end if;
return new;
end;
$$ language plpgsql;
create trigger check_storage_before_insert
before insert on user_files
for each row execute function check_storage_limit();
```
### 1.5 Backups
Self-hosted = you own backups. Set up immediately:
```bash
# Cron job on Coolify host (daily at 3 AM)
0 3 * * * pg_dump -h localhost -U postgres -d postgres | gzip > /backups/dewe_$(date +\%Y\%m\%d).sql.gz
# Retention: keep 30 days
find /backups -name "dewe_*.sql.gz" -mtime +30 -delete
```
Optional: pipe to S3-compatible storage (MinIO on Coolify, or external).
---
## Phase 2: Auth Integration (Frontend)
**Goal:** Add authentication without changing any existing tool UX.
### 2.1 Auth Provider Setup
```
src/
├── contexts/
│ └── AuthContext.js # Supabase auth state
├── hooks/
│ ├── useAuth.js # Auth convenience hook
│ └── useUserFiles.js # File CRUD hook
├── components/
│ ├── AuthModal.js # Login/signup modal (not a page redirect)
│ ├── UserMenu.js # Header avatar/dropdown
│ └── SaveLoadBar.js # Save/Load UI for tools
├── pages/
│ └── AccountPage.js # /account - profile, files, settings
└── lib/
└── supabase.js # Supabase client init
```
### 2.2 "Ghost Auth" Pattern
The core UX principle: **tools never change behavior based on auth state.**
```
┌──────────────────────────────────────────────────┐
│ Tool Editor │
│ │
│ Visitor sees: │ Logged-in user sees: │
│ [Copy] [Download] │ [Copy] [Download] [Save] │
│ │ ↑ │
│ │ only addition │
└──────────────────────────────────────────────────┘
```
**Rules:**
- No redirects to login page from tools. Ever.
- Clicking "Save" when not logged in → opens AuthModal (login/signup) as overlay
- Current editor state is preserved in React state during login
- After successful auth, the save action proceeds automatically
- "Load" button appears in the tool's input section (alongside Create/URL/Paste/Open tabs)
### 2.3 Auth Methods
| Method | Priority | Notes |
|--------|----------|-------|
| Email + Password | P0 | Core auth, must have |
| GitHub OAuth | P1 | Target audience is developers |
| Google OAuth | P2 | Broad reach, nice to have |
| Magic Link (Email) | P2 | Passwordless option |
### 2.4 Header Changes
```
Before (visitor):
┌─────────────────────────────────────────────────┐
│ [Logo] [Tools ▾] [🌙] [☰] │
└─────────────────────────────────────────────────┘
After (visitor):
┌─────────────────────────────────────────────────┐
│ [Logo] [Tools ▾] [🌙] [Sign In] [☰]│
└─────────────────────────────────────────────────┘
After (logged in):
┌─────────────────────────────────────────────────┐
│ [Logo] [Tools ▾] [🌙] [Avatar ▾] [☰]│
└─────────────────────────────────────────────────┘
Avatar dropdown:
├── Account & Files
├── ─────────────
└── Sign Out
```
Minimal. No dashboard. The "Account & Files" link goes to `/account`.
---
## Phase 3: File Save/Load per Tool
**Goal:** Each editor tool gets save/load capability for logged-in users.
### 3.1 Which Tools Support Save/Load
| Tool | Save Content | Content Format | Priority |
|------|-------------|----------------|----------|
| Markdown Editor | MD source text | `{ source: string }` | P0 |
| Invoice Editor | Invoice data | `{ invoice: {...}, template: string }` | P0 |
| Object Editor | JSON/PHP data | `{ input: string, format: string }` | P1 |
| Table Editor | Table data | `{ headers: [...], rows: [...] }` | P1 |
| Beautifier | Code input | `{ code: string, language: string }` | P2 |
| Diff Tool | Both texts | `{ left: string, right: string }` | P2 |
URL Encoder, Base64, and Text Length are **stateless tools** — no save/load needed.
### 3.2 Save/Load UX per Tool
**Save flow:**
1. User works in editor as normal
2. Clicks "Save" → if not logged in, AuthModal opens first
3. Save dialog: enter file name (pre-filled with smart default)
4. Saves to `user_files` via Supabase SDK
5. Toast: "Saved to your account"
**Load flow (new input tab):**
1. Tool input section gets a 5th tab: `[Create] [URL] [Paste] [Open] [My Files]`
2. "My Files" tab shows a list of saved files for this tool type
3. Each entry: name, date, size, [Load] [Delete] buttons
4. Loading replaces current editor content (with unsaved changes warning)
**Smart defaults for file names:**
- Markdown: first heading or "Untitled Document"
- Invoice: "Invoice #{number} - {client_name}"
- Table: "Table - {row_count} rows"
- Object: "Object - {key_count} keys"
### 3.3 `useUserFiles` Hook API
```javascript
const {
files, // File[] for current tool_type
loading, // boolean
saving, // boolean
error, // string | null
saveFile, // (name, content, metadata?) => Promise<File>
updateFile, // (id, content, metadata?) => Promise<File>
deleteFile, // (id) => Promise<void>
loadFiles, // () => Promise<void> (refresh)
storageUsed, // { count, bytes, limit }
} = useUserFiles('markdown');
```
---
## Phase 4: Pro Proxy (Server-Side URL Fetch)
**Goal:** Pro users bypass CORS with a server-side proxy via Supabase Edge Function.
### 4.1 How It Works
```
Free user: Pro user:
Browser → Target API Browser → Edge Function → Target API
↓ ↓
CORS blocked ❌ No CORS ✅
```
### 4.2 Edge Function: `proxy-fetch`
```typescript
// supabase/functions/proxy-fetch/index.ts
import { serve } from "https://deno.land/std/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js";
const BLOCKED_HOSTS = [
/^localhost$/i,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2\d|3[01])\./,
/^192\.168\./,
/^0\./,
/^169\.254\./, // link-local
/^::1$/,
/^fc00:/i, /^fe80:/i, // IPv6 private
];
serve(async (req) => {
// 1. Verify JWT and extract tier
const authHeader = req.headers.get("Authorization");
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: { headers: { Authorization: authHeader } }
});
const { data: { user }, error } = await supabase.auth.getUser();
if (!user || user.app_metadata?.tier !== "pro") {
return new Response(JSON.stringify({ error: "Pro subscription required" }), {
status: 403
});
}
// 2. Parse request
const { url, method, headers, body } = await req.json();
// 3. SSRF protection
const targetUrl = new URL(url);
if (BLOCKED_HOSTS.some(pattern => pattern.test(targetUrl.hostname))) {
return new Response(JSON.stringify({ error: "Blocked host" }), {
status: 400
});
}
// 4. Rate limiting check (50 req/min for Pro)
const { count } = await supabase
.from("proxy_logs")
.select("*", { count: "exact", head: true })
.eq("user_id", user.id)
.gte("created_at", new Date(Date.now() - 60000).toISOString());
if (count >= 50) {
return new Response(JSON.stringify({ error: "Rate limit exceeded (50/min)" }), {
status: 429
});
}
// 5. Forward request
const start = Date.now();
const response = await fetch(url, {
method: method || "GET",
headers: headers || {},
body: body ? JSON.stringify(body) : undefined,
});
const responseBody = await response.text();
// 6. Log usage
await supabase.from("proxy_logs").insert({
user_id: user.id,
target_url: url,
method: method || "GET",
status_code: response.status,
response_size: responseBody.length,
duration_ms: Date.now() - start,
});
// 7. Return response
return new Response(responseBody, {
status: response.status,
headers: {
"Content-Type": response.headers.get("Content-Type") || "application/json",
"Access-Control-Allow-Origin": "https://dewe.dev",
},
});
});
```
### 4.3 Frontend Integration
The existing `AdvancedURLFetch.js` component already has the full UI (method, headers, auth, body, presets). Currently hidden behind `{false && ...}`. Changes needed:
1. **Free users:** Basic fetch (client-side, GET only, CORS-dependent) — current behavior
2. **Pro users:** Toggle reveals advanced panel → requests go through Edge Function
3. Update `features.js` to read tier from `user.app_metadata.tier` instead of static value
4. Presets migration: move from localStorage to `user_files` with `tool_type: 'fetch_preset'`
### 4.4 Security Measures
| Threat | Mitigation |
|--------|-----------|
| SSRF (hitting internal services) | Block private IP ranges, resolve DNS before fetch |
| Abuse (crypto mining, spam) | Rate limiting (50 req/min), response size cap (5 MB) |
| Data exfiltration | Log all proxy requests, alert on anomalies |
| JWT forgery | Verify JWT server-side on every request |
| Cost attack (huge responses) | Stream with size limit, abort after 5 MB |
---
## Phase 5: Account Page
**Goal:** Single page at `/account` for profile, files, and settings. Minimal.
### 5.1 Page Layout
```
/account
┌─────────────────────────────────────────────────┐
│ Profile │
│ ┌─────────────────────────────────────────────┐ │
│ │ [Avatar] display_name tier_badge │ │
│ │ email │ │
│ │ Member since: date │ │
│ │ [Edit Profile] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Storage: ████████░░░░ 2.3 MB / 5 MB (Free) │
│ [Upgrade to Pro] │
│ │
│ My Files [Filter ▾] │
│ ┌─────────────────────────────────────────────┐ │
│ │ 📝 Project README.md 12 KB 2h ago │ │
│ │ 📄 Invoice #042 - Acme 3 KB 1d ago │ │
│ │ 📊 Sales Data Q4 8 KB 3d ago │ │
│ │ { } API Response Sample 1 KB 1w ago │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Each file row: [Open in Editor] [Download] [🗑] │
│ │
│ ────────────────────────────────────────────── │
│ [Sign Out] [Delete Account] │
└─────────────────────────────────────────────────┘
```
### 5.2 "Open in Editor" Flow
Clicking "Open in Editor" on a saved file:
1. Navigates to the corresponding tool (e.g., `/markdown-editor`)
2. Passes file ID via query param: `/markdown-editor?file=uuid`
3. Tool detects `?file=` param → fetches from `user_files` → loads into editor
4. Editor shows "Editing: {filename}" indicator
5. Subsequent saves update the same file (not create new)
---
## Phase 6: localStorage Migration
**Goal:** Seamlessly migrate existing localStorage data to cloud on first login.
### 6.1 Migration Flow
```
User logs in for the first time
Check localStorage for known keys:
• urlFetchPresets
• (any other persisted tool data)
Found data? ──No──→ Done
Yes
Show toast: "We found saved data on this device.
Would you like to sync it to your account?"
┌────┴────┐
Yes No
│ │
▼ ▼
Upload Dismiss
to DB (ask again
+ clear next login)
localStorage
```
### 6.2 Known localStorage Keys to Migrate
| Key | Tool | Migration Target |
|-----|------|-----------------|
| `urlFetchPresets` | AdvancedURLFetch | `user_files` with `tool_type: 'fetch_preset'` |
Currently this is the only localStorage key with user data. As tools add auto-save to localStorage (pre-auth), add those keys here.
---
## Phase 7: Pro Tier & Billing (Future)
**Goal:** Monetize Pro features. Not in initial scope but schema is ready.
### 7.1 Pro Features Summary
| Feature | Free | Pro |
|---------|------|-----|
| All tools | Yes | Yes |
| Copy & Download | Yes | Yes |
| Save files | 20 files, 5 MB | Unlimited, 100 MB |
| URL Fetch | Client-side (CORS-dependent) | Server-side proxy (any API) |
| Fetch presets | localStorage only | Cloud-synced |
| Advanced Fetch UI | Hidden | Full (methods, headers, auth, body) |
| Ads | Yes | No |
### 7.2 Pricing (Recommendation)
**Subscription model** (recurring revenue > one-time):
| Plan | Price | Billing |
|------|-------|---------|
| Monthly | $4.99/mo | Monthly |
| Annual | $39.99/yr (~$3.33/mo) | Annual |
**Why subscription over one-time:**
- Server costs are ongoing (Coolify hosting, proxy bandwidth)
- Aligns cost with value delivered
- Predictable revenue
**Payment provider:** Stripe (or Lemon Squeezy for simpler tax handling).
### 7.3 Tier Enforcement Architecture
```
Client-side (fast):
user.app_metadata.tier → show/hide UI elements
Server-side (secure):
JWT claims → Edge Function checks tier before processing
DB triggers → sync tier changes to JWT claims
Storage limits → DB trigger on insert
```
The `sync_tier_to_claims` trigger (Phase 1 schema) ensures that when you update `profiles.tier`, the JWT custom claim is updated. The user's next token refresh picks up the new tier automatically.
---
## Implementation Order
```
Phase 1: Supabase on Coolify ░░░░░░░░ ~1 day
└─ Deploy, configure, run schema
└─ Verify connection from localhost
Phase 2: Auth Integration ░░░░░░░░░░░░ ~2 days
└─ supabase.js client setup
└─ AuthContext + useAuth hook
└─ AuthModal (login/signup overlay)
└─ Header UserMenu
└─ /account page (basic)
Phase 3: File Save/Load ░░░░░░░░░░░░░░░░ ~3 days
└─ useUserFiles hook
└─ SaveLoadBar component
└─ "My Files" tab in each tool
└─ Account page file manager
└─ Markdown Editor integration (first)
└─ Invoice Editor integration
└─ Remaining tools
Phase 4: Pro Proxy ░░░░░░░░░░░░ ~2 days
└─ Edge Function deployment
└─ SSRF protection
└─ Rate limiting
└─ AdvancedURLFetch rewire
└─ Feature flags from JWT
Phase 5: localStorage Migration ░░░░ ~0.5 day
└─ Detection + migration prompt
Phase 6: Polish & Testing ░░░░░░░░ ~1.5 days
└─ Error states, loading states
└─ Mobile responsive check
└─ Auth edge cases (expired session, etc.)
Total estimate: ~10 days
```
---
## Technical Decisions & Rationale
### Why JSONB in Postgres (not Supabase Storage)?
All saved content is text-based and < 2 MB. JSONB gives us:
- Queryable content (search inside saved files later)
- Indexable fields (filter by tool_type, sort by date)
- Single table, single query — no separate file download step
- Atomic operations (save file + update metadata in one transaction)
- Simpler backup (one pg_dump gets everything)
Supabase Storage (S3-compatible) would be better for binary files (images, PDFs > 10 MB). We don't have that use case.
### Why Edge Functions (not a separate Node.js proxy)?
- Already included in self-hosted Supabase — no extra deployment
- Direct access to Supabase Auth (JWT verification built-in)
- Deno runtime is secure by default (explicit permissions)
- Scales with the Supabase instance
- One less service to maintain on Coolify
### Why Modal Login (not a /login page)?
- User is mid-work in an editor when they hit "Save"
- Redirecting to /login would lose their unsaved editor state
- Modal keeps React state intact — after login, save proceeds immediately
- Better conversion: lower friction = more signups
### Why Custom Claims for Tier (not DB lookup per request)?
- JWT claims are verified locally — zero network latency
- Edge Function doesn't need a DB query to check tier
- DB trigger keeps claims in sync automatically
- Standard Supabase pattern, well-documented
---
## Risks & Mitigations
| Risk | Impact | Mitigation |
|------|--------|-----------|
| Supabase self-hosted instability | High | Monitor with Coolify health checks, set up alerts, daily backups |
| Proxy abuse (SSRF, scraping) | High | IP blocklist, rate limiting, URL validation, response size cap |
| Storage costs grow | Medium | Enforce limits per tier, compress JSONB, monitor `storage_used_bytes` |
| Auth session expired mid-edit | Medium | Silent token refresh, save to localStorage as fallback, retry on 401 |
| Migration from localStorage fails | Low | Non-destructive: keep localStorage as fallback, retry option |
| Coolify resource limits | Medium | Start with 4 GB RAM, monitor, scale vertically as needed |
---
## Open Questions
1. **OAuth providers** — GitHub OAuth is ideal for dev audience. Worth adding Google too for broader reach? Both are easy with Supabase Auth.
2. **File sharing** — Should saved files be shareable via public link? (e.g., share a markdown preview). Not in initial scope, but schema supports adding a `is_public` flag later.
3. **Auto-save** — Should tools auto-save to cloud every N seconds for logged-in users? Or only manual save? Recommendation: manual save only (simpler, less backend load, user controls what gets saved).
4. **Offline support** — If Supabase is down, tools still work (client-side). But save/load fails. Should we queue saves in localStorage and sync when back online?
5. **Delete account** — GDPR requires account deletion. The `on delete cascade` in the schema handles this. Need a confirmation flow in the UI.
---
## File Structure Changes
```
src/
├── lib/
│ └── supabase.js # NEW: Supabase client init
├── contexts/
│ └── AuthContext.js # NEW: Auth state provider
├── hooks/
│ ├── useAuth.js # NEW: Auth convenience hook
│ ├── useUserFiles.js # NEW: File CRUD operations
│ ├── useAnalytics.js
│ └── useNavigationGuard.js
├── components/
│ ├── AuthModal.js # NEW: Login/signup modal overlay
│ ├── UserMenu.js # NEW: Avatar dropdown in header
│ ├── SaveLoadBar.js # NEW: Save/Load buttons for tools
│ ├── MyFilesTab.js # NEW: "My Files" input tab
│ ├── StorageMeter.js # NEW: Storage usage indicator
│ ├── AdvancedURLFetch.js # MODIFY: use proxy for Pro
│ ├── ProBadge.js # KEEP: already exists
│ └── Layout.js # MODIFY: add UserMenu to header
├── pages/
│ ├── AccountPage.js # NEW: /account
│ └── ...existing tools... # MODIFY: add save/load integration
├── config/
│ ├── features.js # MODIFY: read tier from JWT claims
│ └── tools.js # KEEP
└── App.js # MODIFY: wrap with AuthProvider, add /account route
supabase/
├── migrations/
│ └── 001_initial_schema.sql # NEW: all tables, RLS, triggers
└── functions/
└── proxy-fetch/
└── index.ts # NEW: CORS proxy edge function
```

55
PROJECT_ROADMAP.md Normal file → Executable file
View File

@@ -131,37 +131,36 @@ Build a comprehensive suite of developer tools with a focus on:
---
#### Priority 3: AdSense Integration 💵
**Status:** ⏳ In Progress (Awaiting approval)
#### Priority 3: Adsterra Integration 💵
**Status:** ✅ Completed
**Timeline:** 1 day
**Impact:** HIGH - Start earning revenue
**Steps:**
1. Apply for Google AdSense account
2. Add AdSense script to `index.html`
3. Create ad units in AdSense dashboard
4. Implement ad components with AdSense code
5. Test ad display and responsiveness
6. Monitor ad performance
1. Apply for Adsterra account
2. Add Adsterra anti-adblock script to `index.html` and components
3. Create ad units in Adsterra dashboard
4. Implement ad components with Adsterra code
5. Test ad display and responsiveness
**Ad Units Needed:**
- Desktop Sidebar 1 (300x250)
- Desktop Sidebar 2 (300x250)
- Desktop Sidebar 3 (300x250)
- Mobile Bottom Banner (320x50)
- Desktop Sidebar 1 (300x250)
- Desktop Sidebar 2 (300x250)
- Desktop Sidebar 3 (300x250)
- Mobile Bottom Banner (320x50)
**Compliance:**
- Add Privacy Policy page
- Add Terms of Service page
- Cookie consent banner (if required)
- GDPR compliance (if applicable)
- Add Privacy Policy page
- Add Terms of Service page
- Cookie consent banner
- GDPR compliance
---
### Phase 2: Content Expansion (Week 3-6)
#### Markdown Editor 📝
**Status:** ✅ Completed (October 22, 2025)
**Status:** ✅ Completed (June 14, 2026)
**Timeline:** 1-2 weeks
**Impact:** HIGH - Major new feature, attracts new users
@@ -170,29 +169,25 @@ Build a comprehensive suite of developer tools with a focus on:
- Create New (empty/sample)
- URL Import (fetch markdown from GitHub, Gist, etc.)
- Paste (markdown, HTML auto-convert, plain text)
- Open Files (.md, .txt, .html, .docx)
- Open Files (.md, .txt)
- **Editor:**
- CodeMirror with markdown syntax highlighting
- Split view (editor + live preview)
- View modes: Split, Editor Only, Preview Only, Fullscreen
- Markdown toolbar (Bold, Italic, Headers, Links, Images, Code, Lists, Tables)
- Line numbers
- Tiptap-powered WYSIWYG Rich Text Editor
- Fallback Raw Markdown CodeMirror Editor
- View modes: Read, Edit, Markdown
- Toolbar (Bold, Italic, Headers, Links, Images, Code, Lists, Tables)
- Word count & statistics
- **Preview:**
- Live rendering (marked + DOMPurify)
- Auto-generated HTML parsing
- Syntax highlighting for code blocks (highlight.js)
- GitHub Flavored Markdown support
- Table of Contents auto-generation
- Mermaid diagram rendering (in preview)
- **Export:**
- Markdown (.md) - Standard, GFM, CommonMark
- HTML (.html) - Standalone with CSS
- Markdown (.md)
- HTML (.html)
- HTML Content Body
- Plain Text (.txt)
- PDF (.pdf) - via html2pdf
- DOCX (.docx) - via docx library
**Advanced Features (Post-MVP):**
- Tables support (GitHub-style)

0
README.md Normal file → Executable file
View File

0
SEO_IMPROVEMENT_PLAN.md Normal file → Executable file
View File

368
TODO.md Normal file → Executable file
View File

@@ -179,310 +179,168 @@
---
### 💵 Priority 3: AdSense Integration (1 day) - ⏳ IN PROGRESS
### 💵 Priority 3: Adsterra Integration (1 day) - ✅ COMPLETED
#### AdSense Setup
- [ ] Apply for Google AdSense account
- [ ] Provide website URL
- [ ] Provide contact information
- [ ] Wait for approval (can take 1-3 days)
- [ ] Verify site ownership (add verification code)
#### Adsterra Setup
- [x] Apply for Adsterra publisher account
- [x] Add website URL
- [x] Receive approval
#### Ad Units Creation
- [ ] Log in to AdSense dashboard
- [ ] Create ad unit: Desktop Sidebar 1 (300x250)
- [ ] Create ad unit: Desktop Sidebar 2 (300x250)
- [ ] Create ad unit: Desktop Sidebar 3 (300x250)
- [ ] Create ad unit: Mobile Bottom Banner (320x50)
- [ ] Copy ad unit codes
- [x] Create ad unit: Desktop Sidebar 1 (300x250)
- [x] Create ad unit: Desktop Sidebar 2 (300x250)
- [x] Create ad unit: Mobile Bottom Banner (320x50)
- [x] Copy ad unit codes
- [x] Request Anti-Adblock custom domain
#### Implementation
- [ ] Add AdSense script to `public/index.html`
```html
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXX"
crossorigin="anonymous"></script>
```
- [ ] Update `AdBlock.jsx` with AdSense code
```jsx
<ins className="adsbygoogle"
style={{ display: 'block' }}
data-ad-client="ca-pub-XXXXXXXX"
data-ad-slot="XXXXXXXXXX"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
```
- [ ] Update `MobileAdBanner.jsx` with AdSense code
- [ ] Initialize ads: `(adsbygoogle = window.adsbygoogle || []).push({});`
- [x] Update `AdBlock.jsx` with Adsterra iframe code
- [x] Update `MobileAdBanner.jsx` with Adsterra iframe code
- [x] Update custom Anti-Adblock domain (`downconvenientmagnetic.com`)
#### Testing
- [ ] Test ad display on desktop
- [ ] Test ad display on mobile
- [ ] Verify ads load correctly
- [ ] Check for console errors
- [ ] Test with ad blocker (should show message)
- [ ] Test on different browsers (Chrome, Firefox, Safari)
- [ ] Test on different devices
- [x] Test ad display on desktop
- [x] Test ad display on mobile
- [x] Verify ads load correctly
- [x] Check for console errors
- [x] Test on different devices
#### Monitoring
- [ ] Set up AdSense reporting
- [ ] Monitor ad impressions
- [ ] Monitor ad clicks
- [ ] Monitor ad revenue
- [ ] Track CTR (Click-Through Rate)
- [ ] Identify best-performing ad units
- [x] Monitor ad impressions
- [x] Monitor ad clicks
- [x] Track CTR (Click-Through Rate)
#### Compliance
- [ ] Create Privacy Policy page
- [ ] Data collection disclosure
- [ ] Cookie usage disclosure
- [ ] Third-party services (AdSense)
- [ ] User rights (GDPR)
- [ ] Create Terms of Service page
- [ ] Acceptable use policy
- [ ] Disclaimer
- [ ] Limitation of liability
- [ ] Add cookie consent banner (if required)
- [ ] Show on first visit
- [ ] Allow accept/decline
- [ ] Store preference
- [ ] Add "About Ads" link in footer
- [ ] Add "Privacy Policy" link in footer
- [ ] Add "Terms of Service" link in footer
#### Optimization
- [ ] Test different ad placements
- [ ] Test different ad sizes
- [ ] Monitor ad viewability
- [ ] Optimize for higher CTR
- [ ] A/B test ad positions (optional)
- [x] Create Privacy Policy page
- [x] Data collection disclosure
- [x] Cookie usage disclosure
- [x] Third-party services (Adsterra)
- [x] User rights (GDPR)
- [x] Create Terms of Service page
- [x] Acceptable use policy
- [x] Disclaimer
- [x] Limitation of liability
- [x] Add cookie consent banner
- [x] Add "Privacy Policy" link in footer
- [x] Add "Terms of Service" link in footer
---
## 📋 Phase 2: Content Expansion
### 📝 Markdown Editor - MVP (1-2 weeks) - ✅ COMPLETED (Oct 22, 2025)
### 📝 Markdown Editor - MVP (1-2 weeks) - ✅ COMPLETED
#### Planning
- [ ] Finalize feature list for MVP
- [ ] Design UI mockup (split view)
- [ ] Plan component structure
- [ ] Choose markdown parser (marked vs markdown-it)
- [ ] Plan export formats
- [x] Finalize feature list for MVP
- [x] Design UI mockup (WYSIWYG layout)
- [x] Plan component structure
- [x] Implement Tiptap integration
- [x] Plan export formats
#### Project Setup
- [ ] Create `MarkdownEditor.jsx` page
- [ ] Set up routing (`/markdown-editor`)
- [ ] Add to navigation menu
- [ ] Add to homepage tools list
- [x] Create `MarkdownEditor.jsx` page
- [x] Create `RichMarkdownEditor.js` component
- [x] Set up routing (`/markdown-editor`)
- [x] Add to navigation menu
- [x] Add to homepage tools list
#### Input Section
- [ ] Implement Create New tab
- [ ] Start Empty button
- [ ] Load Sample button (with example markdown)
- [ ] Tip box
- [ ] Implement URL tab
- [ ] Use AdvancedURLFetch component
- [ ] Support GitHub raw URLs
- [ ] Support Gist URLs
- [ ] Test with various markdown sources
- [ ] Implement Paste tab
- [ ] CodeMirror editor
- [ ] Markdown syntax highlighting
- [ ] Auto-detect markdown
- [ ] Parse button
- [ ] Collapse after parse
- [ ] Implement Open tab
- [ ] Support .md files
- [ ] Support .txt files
- [ ] Support .html files (convert to markdown)
- [ ] Support .docx files (convert to markdown)
- [ ] Auto-load on file selection
- [x] Implement Create New tab
- [x] Start Empty button
- [x] Load Sample button (with example markdown)
- [x] Tip box
- [x] Implement URL tab
- [x] Use AdvancedURLFetch component
- [x] Support GitHub raw URLs
- [x] Support Gist URLs
- [x] Test with various markdown sources
- [x] Implement Paste tab
- [x] CodeMirror editor
- [x] Markdown syntax highlighting
- [x] Parse button
- [x] Implement Open tab
- [x] Support .md files
- [x] Support .txt files
- [x] Auto-load on file selection
#### Editor Section
- [ ] Set up CodeMirror for markdown
- [ ] Install @codemirror/lang-markdown
- [ ] Configure markdown mode
- [ ] Add syntax highlighting
- [ ] Add line numbers
- [ ] Add line wrapping
- [ ] Implement split view layout
- [ ] Editor pane (left)
- [ ] Preview pane (right)
- [ ] Resizable divider (optional)
- [ ] Implement view mode toggle
- [ ] Split view (default)
- [ ] Editor only
- [ ] Preview only
- [ ] Fullscreen mode
- [ ] Add markdown toolbar
- [ ] Bold button (Ctrl+B)
- [ ] Italic button (Ctrl+I)
- [ ] H1 button
- [ ] H2 button
- [ ] H3 button
- [ ] Link button (Ctrl+K)
- [ ] Image button
- [ ] Code button (Ctrl+`)
- [ ] Quote button
- [ ] Unordered list button
- [ ] Ordered list button
- [ ] Table button
- [ ] Add editor features
- [ ] Word count
- [ ] Character count
- [ ] Line count
- [ ] Reading time estimate
- [x] Implement WYSIWYG Editor (Tiptap)
- [x] Install `@tiptap/react` and `tiptap-markdown`
- [x] Add standard text formatting (bold, italic, strike)
- [x] Add block formatting (headers, quotes, lists)
- [x] Add inline code and code block extensions
- [x] Set up Lowlight syntax highlighting
- [x] Implement view mode toggle
- [x] Read mode (Clean preview default)
- [x] Edit mode (WYSIWYG Tiptap)
- [x] Markdown mode (Raw CodeMirror)
- [x] Fullscreen mode
- [x] Add editor features
- [x] Word count
- [x] Character count
- [x] Line count
- [x] Reading time estimate
#### Preview Section
- [ ] Set up markdown parser (marked)
- [ ] Install marked
- [ ] Install DOMPurify
- [ ] Configure marked options
- [ ] Implement live preview
- [ ] Real-time rendering
- [ ] Debounce for performance
- [ ] Scroll sync (optional)
- [ ] Add syntax highlighting for code blocks
- [ ] Install highlight.js
- [ ] Configure languages
- [ ] Apply highlighting
- [ ] Add GitHub Flavored Markdown support
- [ ] Tables
- [ ] Strikethrough
- [ ] Task lists
- [ ] Autolinks
- [ ] Implement Table of Contents
- [ ] Auto-generate from headers
- [ ] Clickable links
- [ ] Collapsible (optional)
- [ ] Add mermaid diagram rendering
- [ ] Install mermaid
- [ ] Detect mermaid code blocks
- [ ] Render diagrams
- [ ] Error handling
- [x] Build robust HTML to Markdown / Markdown to HTML sync
- [x] Set up markdown fallback parser (marked)
- [x] GitHub Flavored Markdown support (Tables, task lists)
- [x] Custom code block rendering with Copy button in Read mode
#### Export Section
- [ ] Create collapsible export section
- [ ] Implement Markdown export
- [ ] Standard Markdown
- [ ] GitHub Flavored Markdown
- [ ] CommonMark
- [ ] Copy to clipboard
- [ ] Download as .md file
- [ ] Implement HTML export
- [ ] Standalone HTML with CSS
- [ ] Inline styles
- [ ] Include syntax highlighting CSS
- [ ] Copy to clipboard
- [ ] Download as .html file
- [ ] Implement Plain Text export
- [ ] Strip all formatting
- [ ] Copy to clipboard
- [ ] Download as .txt file
- [ ] Implement PDF export
- [ ] Install html2pdf.js
- [ ] Convert HTML to PDF
- [ ] Maintain formatting
- [ ] Download as .pdf file
- [ ] Implement DOCX export
- [ ] Install docx library
- [ ] Convert markdown to DOCX
- [ ] Maintain formatting
- [ ] Download as .docx file
#### Conversion Features
- [ ] HTML to Markdown conversion
- [ ] Install turndown
- [ ] Convert on paste (if HTML detected)
- [ ] Convert on file open (.html)
- [ ] DOCX to Markdown conversion
- [ ] Install mammoth.js
- [ ] Convert on file open (.docx)
- [ ] Extract text and formatting
#### Usage Tips
- [ ] Create collapsible Usage Tips section
- [ ] Add Input Methods tips
- [ ] Add Editor Features tips
- [ ] Add Markdown Syntax tips
- [ ] Add Export Options tips
- [ ] Add Data Privacy tips
- [x] Create collapsible export section
- [x] Implement Markdown export
- [x] Copy to clipboard
- [x] Download as .md file
- [x] Implement HTML export
- [x] Standalone HTML with CSS
- [x] Download as .html file
- [x] Implement HTML Content export
- [x] Strip React/Tailwind wrapper classes
- [x] Download body HTML only
- [x] Implement Plain Text export
- [x] Strip markdown syntax via regex
- [x] Download as .txt file
- [x] Implement PDF export
- [x] Install html2pdf.js
- [x] Inject CSS print media rules to prevent pre overflow
- [x] Download as .pdf file
#### Data Loss Prevention
- [ ] Implement hasUserData() function
- [ ] Implement hasModifiedData() function
- [ ] Add confirmation modal for tab changes
- [ ] Add confirmation for Create New buttons
- [x] Implement `hasUserData()` function
- [x] Implement `hasModifiedData()` function
- [x] Add confirmation modal for tab changes
- [x] Add confirmation for Create New buttons
#### Testing
- [ ] Test all input methods
- [ ] Test markdown rendering
- [ ] Test all export formats
- [ ] Test HTML to Markdown conversion
- [ ] Test DOCX import
- [ ] Test mermaid diagrams
- [ ] Test code syntax highlighting
- [ ] Test Table of Contents
- [ ] Test view mode toggle
- [ ] Test toolbar buttons
- [ ] Test keyboard shortcuts
- [ ] Test responsive design
- [ ] Test dark mode
- [ ] Test on mobile devices
#### Documentation
- [ ] Add to EDITOR_TOOL_GUIDE.md
- [ ] Create user guide
- [ ] Add screenshots
- [ ] Create tutorial video (optional)
- [x] Test all input methods
- [x] Test Tiptap to Markdown serialization
- [x] Test all export formats
- [x] Test code syntax highlighting
- [x] Test view mode toggle
- [x] Test toolbar buttons
- [x] Test responsive design
- [x] Test dark mode
---
### 📝 Markdown Editor - Post-MVP (Future)
#### Advanced Markdown Features
- [ ] Add table support (GitHub-style)
- [ ] Add task lists (checkboxes)
- [ ] Add footnotes support
- [ ] Add emoji support (:smile:)
- [ ] Add emoji support (WYSIWYG picker)
- [ ] Add math equations (KaTeX)
- [ ] Install katex
- [ ] Detect math blocks
- [ ] Render equations
#### Templates
- [ ] Create README.md template
- [ ] Create Documentation template
- [ ] Create Blog post template
- [ ] Create Meeting notes template
- [ ] Create Project proposal template
- [ ] Add template selector UI
- [ ] Allow custom templates
- [ ] Add mermaid diagram rendering
- [ ] Implement Table of Contents auto-generation
#### Utilities
- [ ] Add markdown linter
- [ ] Check for common issues
- [ ] Suggest improvements
- [ ] Show warnings
- [ ] Add link checker
- [ ] Validate URLs
- [ ] Check for broken links
- [ ] Show status
- [ ] Add format beautifier
- [ ] Clean up markdown
- [ ] Consistent formatting
- [ ] Fix indentation
- [ ] Add image optimizer
- [ ] Compress images
- [ ] Convert to base64
- [ ] Optimize for web
#### Enhanced Features
- [ ] Add keyboard shortcuts
- [ ] Add auto-save (localStorage)
- [ ] Add export history
- [ ] Add version history
- [ ] Add collaborative editing (future)
---

0
nixpacks.toml Normal file → Executable file
View File

45334
package-lock.json generated Normal file → Executable file

File diff suppressed because it is too large Load Diff

21
package.json Normal file → Executable file
View File

@@ -18,22 +18,37 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/typography": "^0.5.20",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@tiptap/extension-code-block-lowlight": "^3.26.1",
"@tiptap/extension-image": "^3.26.1",
"@tiptap/extension-link": "^3.26.1",
"@tiptap/extension-table": "^3.26.1",
"@tiptap/extension-table-cell": "^3.26.1",
"@tiptap/extension-table-header": "^3.26.1",
"@tiptap/extension-table-row": "^3.26.1",
"@tiptap/extension-task-item": "^3.26.1",
"@tiptap/extension-task-list": "^3.26.1",
"@tiptap/react": "^3.26.1",
"@tiptap/starter-kit": "^3.26.1",
"@uiw/react-codemirror": "^4.25.1",
"codemirror": "^6.0.2",
"diff-match-patch": "^1.0.5",
"dompurify": "^3.3.0",
"file-saver": "^2.0.5",
"highlight.js": "^11.11.1",
"html-to-image": "^1.11.13",
"html2pdf.js": "^0.12.1",
"js-beautify": "^1.15.4",
"jspdf": "^3.0.3",
"jspdf-autotable": "^5.0.2",
"lowlight": "^3.3.0",
"lucide-react": "^0.540.0",
"marked": "^16.4.1",
"marked-emoji": "^2.0.1",
"mermaid": "^11.15.0",
"papaparse": "^5.5.3",
"react": "18.3.1",
"react-diff-view": "^3.3.2",
@@ -41,9 +56,12 @@
"react-helmet-async": "^2.0.5",
"react-router-dom": "6.26.2",
"react-snap": "^1.23.0",
"react-zoom-pan-pinch": "^4.0.3",
"reactflow": "^11.11.4",
"serialize-javascript": "^6.0.0",
"serve": "^14.2.4",
"tailwindcss-typography": "^3.1.0",
"tiptap-markdown": "^0.9.0",
"turndown": "^7.2.1",
"web-vitals": "^2.1.4"
},
@@ -89,8 +107,7 @@
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
"react-app"
]
},
"browserslist": {

0
postcss.config.js Normal file → Executable file
View File

0
public/ads.txt Normal file → Executable file
View File

0
public/android-chrome-192x192.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

0
public/android-chrome-512x512.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

0
public/apple-touch-icon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

58
public/data/commits.json Normal file → Executable file
View File

@@ -1,5 +1,63 @@
{
"changelog": [
{
"date": "2026-06-14",
"changes": [
{
"datetime": "2026-06-14T10:00:00+07:00",
"type": "feature",
"title": "Major Markdown Editor Rewrite: WYSIWYG Experience",
"description": "Completely rebuilt the Markdown Editor to feature a true WYSIWYG (What You See Is What You Get) interface using Tiptap. You can now edit rich text visually like a Word document, while seamlessly converting back and forth to raw Markdown and clean HTML."
},
{
"datetime": "2026-06-14T09:30:00+07:00",
"type": "enhancement",
"title": "Object Editor: Data Preview & Multidimensional Search",
"description": "The Object Editor now defaults to a fast Read-Only preview when you paste JSON data. We also added an incredibly powerful multidimensional search bar that instantly filters, highlights, and expands nested nodes matching your query."
}
]
},
{
"date": "2026-02-18",
"changes": [
{
"datetime": "2026-02-18T15:00:00+07:00",
"type": "feature",
"title": "Performance Boost with Code Splitting",
"description": "Dramatically improved page load times by implementing lazy loading for all tool pages. Each tool now loads only when you need it, reducing initial bundle size by over 50%. Experience faster navigation between tools with a smooth loading transition."
},
{
"datetime": "2026-02-18T14:00:00+07:00",
"type": "enhancement",
"title": "WCAG AA Accessibility Improvements",
"description": "Made the site more accessible for all users: added proper ARIA labels to navigation buttons, improved keyboard navigation, enhanced screen reader support with live error announcements, and fixed 300+ low-contrast text instances to meet WCAG AA standards."
},
{
"datetime": "2026-02-18T13:00:00+07:00",
"type": "feature",
"title": "New Advertising Partnership with Adsterra",
"description": "Migrated from Google AdSense to Adsterra for better ad performance. Desktop users see a non-intrusive sidebar ad, while mobile users get a dismissible bottom banner. All ads respect your privacy and GDPR consent preferences."
},
{
"datetime": "2026-02-18T12:00:00+07:00",
"type": "feature",
"title": "Onidel Affiliate Partnership",
"description": "Partnered with Onidel to bring you professional development services. Check out the sidebar on desktop for special offers and services from our trusted partner."
},
{
"datetime": "2026-02-18T11:00:00+07:00",
"type": "enhancement",
"title": "Code Quality & Performance Cleanup",
"description": "Removed 150+ debug console statements, deleted deprecated packages, eliminated duplicate dependencies, and cleaned up dead code. The codebase is now leaner and more maintainable."
},
{
"datetime": "2026-02-18T10:00:00+07:00",
"type": "enhancement",
"title": "Improved Configuration Management",
"description": "Moved Google Analytics configuration to environment variables for better security and easier deployment. Created .env.example for seamless local development setup."
}
]
},
{
"date": "2025-10-22",
"changes": [

0
public/data/currencies.json Normal file → Executable file
View File

0
public/favicon-16x16.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 962 B

After

Width:  |  Height:  |  Size: 962 B

0
public/favicon-32x32.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

0
public/favicon.ico Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

0
public/icon-192x192.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

0
public/icon-512x512.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

0
public/images/onidel-banner.webp Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

0
public/index.html Normal file → Executable file
View File

0
public/logo.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

0
public/manifest.json Normal file → Executable file
View File

0
public/robots.txt Normal file → Executable file
View File

0
public/sitemap.xml Normal file → Executable file
View File

101
src/App.js Normal file → Executable file
View File

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

14
src/components/AdBlock.js Normal file → Executable file
View File

@@ -1,6 +1,10 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef } from "react";
const AdBlock = ({ className = '', adKey = 'e0ca7c61c83457f093bbc2e261b43d31' }) => {
const AdBlock = ({
className = "",
adKey = "e0ca7c61c83457f093bbc2e261b43d31",
adDomain = "downconvenientmagnetic.com",
}) => {
const iframeRef = useRef(null);
useEffect(() => {
@@ -28,18 +32,18 @@ const AdBlock = ({ className = '', adKey = 'e0ca7c61c83457f093bbc2e261b43d31' })
'params' : {}
};
</script>
<script type="text/javascript" src="https://bustleplaguereed.com/${adKey}/invoke.js"></script>
<script type="text/javascript" src="https://${adDomain}/${adKey}/invoke.js"></script>
</body>
</html>
`);
doc.close();
}, [adKey]);
}, [adKey, adDomain]);
return (
<iframe
ref={iframeRef}
className={`bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${className}`}
style={{ width: '300px', height: '250px', border: 'none' }}
style={{ width: "300px", height: "250px", border: "none" }}
title="Advertisement"
sandbox="allow-scripts allow-same-origin"
/>

0
src/components/AdColumn.js Normal file → Executable file
View File

6
src/components/AdvancedURLFetch.js Normal file → Executable file
View File

@@ -454,7 +454,7 @@ const AdvancedURLFetch = ({
</div>
)}
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p className="text-xs text-gray-600 dark:text-gray-600 mt-1">
💡 Tip: Toggle between raw JSON and visual tree editor for easier editing
</p>
</div>
@@ -495,7 +495,7 @@ const AdvancedURLFetch = ({
>
<FolderOpen className="h-4 w-4" />
<span className="font-medium">{preset.name}</span>
<span className="text-xs text-gray-500">({preset.method})</span>
<span className="text-xs text-gray-600">({preset.method})</span>
</button>
<button
onClick={() => deletePreset(index)}
@@ -518,7 +518,7 @@ const AdvancedURLFetch = ({
)
)}
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-xs text-gray-600 dark:text-gray-600">
{showAdvanced
? 'Configure HTTP method, headers, authentication, and request body for API testing'
: 'Enter any URL that returns JSON data. Examples: Telegram Bot API, JSONPlaceholder, GitHub API, etc.'

0
src/components/AffiliateBlock.js Normal file → Executable file
View File

View File

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

0
src/components/CodeEditor.js Normal file → Executable file
View File

2
src/components/CodeMirrorEditor.js Normal file → Executable file
View File

@@ -217,7 +217,7 @@ const CodeMirrorEditor = ({
}
}, 50);
}}
className="absolute bottom-2 right-2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-600 shadow-sm z-10"
className="absolute bottom-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 border border-gray-300 dark:border-gray-600 shadow-sm z-10"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? (

4
src/components/ConsentBanner.js Normal file → Executable file
View File

@@ -80,7 +80,7 @@ const ConsentBanner = () => {
>
Privacy Policy
</Link>
<span className="text-slate-400"></span>
<span className="text-slate-600"></span>
<Link
to="/terms"
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 underline"
@@ -123,7 +123,7 @@ const ConsentBanner = () => {
</h3>
<button
onClick={() => setShowCustomize(false)}
className="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
className="p-1 text-slate-600 hover:text-slate-600 dark:hover:text-slate-300"
>
<X className="h-5 w-5" />
</button>

2
src/components/CopyButton.js Normal file → Executable file
View File

@@ -23,7 +23,7 @@ const CopyButton = ({ text, className = '' }) => {
{copied ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4 text-gray-600 dark:text-gray-400" />
<Copy className="h-4 w-4 text-gray-600 dark:text-gray-600" />
)}
</button>
);

4
src/components/ErrorBoundary.js Normal file → Executable file
View File

@@ -40,7 +40,7 @@ class ErrorBoundary extends React.Component {
Something went wrong
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
<p className="text-sm text-gray-600 dark:text-gray-600 mb-4">
The application encountered an error. This might be due to browser compatibility issues.
</p>
@@ -60,7 +60,7 @@ class ErrorBoundary extends React.Component {
</button>
</div>
<div className="mt-4 text-xs text-gray-500 dark:text-gray-400">
<div className="mt-4 text-xs text-gray-600 dark:text-gray-600">
If you're using Telegram's built-in browser, try opening this link in your default browser for better compatibility.
</div>
</div>

View File

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

267
src/components/Layout.js Normal file → Executable file
View File

@@ -1,18 +1,30 @@
import React, { useState, useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import ToolSidebar from './ToolSidebar';
import NavigationConfirmModal from './NavigationConfirmModal';
import useNavigationGuard from '../hooks/useNavigationGuard';
import { Menu, X, ChevronDown, Terminal, Sparkles, Home } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
import SEOHead from './SEOHead';
import ConsentBanner from './ConsentBanner';
import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
import { useAnalytics } from '../hooks/useAnalytics';
import React, { useState, useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";
import ToolSidebar from "./ToolSidebar";
import NavigationConfirmModal from "./NavigationConfirmModal";
import useNavigationGuard from "../hooks/useNavigationGuard";
import { Menu, X, ChevronDown, Terminal, Sparkles, Home } from "lucide-react";
import ThemeToggle from "./ThemeToggle";
import SEOHead from "./SEOHead";
import ConsentBanner from "./ConsentBanner";
import {
NON_TOOLS,
TOOLS,
SITE_CONFIG,
getCategoryConfig,
} from "../config/tools";
import { useAnalytics } from "../hooks/useAnalytics";
const Layout = ({ children }) => {
const location = useLocation();
const { showModal, pendingNavigation, handleConfirm, handleCancel, hasUnsavedData, navigateWithGuard } = useNavigationGuard();
const {
showModal,
pendingNavigation,
handleConfirm,
handleCancel,
hasUnsavedData,
navigateWithGuard,
} = useNavigationGuard();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const dropdownRef = useRef(null);
@@ -32,9 +44,9 @@ const Layout = ({ children }) => {
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
@@ -45,10 +57,10 @@ const Layout = ({ children }) => {
}, [location.pathname]);
// Check if we're on a tool page (not homepage)
const isToolPage = location.pathname !== '/';
const isToolPage = location.pathname !== "/";
// Check if we're on invoice preview page (no sidebar needed)
const isInvoicePreviewPage = location.pathname === '/invoice-preview';
const isInvoicePreviewPage = location.pathname === "/invoice-preview";
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex flex-col">
@@ -57,9 +69,18 @@ const Layout = ({ children }) => {
{/* Header */}
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
<div className={isToolPage ? "px-4 sm:px-6 lg:px-8" : "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"}>
<div
className={
isToolPage
? "px-4 sm:px-6 lg:px-8"
: "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"
}
>
<div className="flex justify-between items-center h-16">
<button onClick={() => navigateWithGuard('/')} className="flex items-center group">
<button
onClick={() => navigateWithGuard("/")}
className="flex items-center group"
>
<div className="relative">
<div className="absolute inset-0 rounded-lg blur opacity-20 group-hover:opacity-40 transition-opacity"></div>
<div className="relative p-2">
@@ -67,11 +88,11 @@ const Layout = ({ children }) => {
src="/logo.svg"
alt={SITE_CONFIG.title}
className="h-8 w-auto"
style={{ maxWidth: '150px' }}
style={{ maxWidth: "150px" }}
onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
e.target.style.display = "none";
e.target.nextSibling.style.display = "flex";
}}
/>
<div className="hidden items-center space-x-3">
@@ -91,12 +112,12 @@ const Layout = ({ children }) => {
<button
onClick={() => {
setIsDropdownOpen(false);
navigateWithGuard('/');
navigateWithGuard("/");
}}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 ${
isActive('/')
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-white/50 dark:hover:bg-slate-700/50'
isActive("/")
? "bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg"
: "text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-white/50 dark:hover:bg-slate-700/50"
}`}
>
<Home className="h-4 w-4" />
@@ -108,12 +129,16 @@ const Layout = ({ children }) => {
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300"
aria-expanded={isDropdownOpen}
aria-haspopup="true"
>
<Sparkles className="h-4 w-4" />
<span>Tools</span>
<ChevronDown className={`h-4 w-4 transition-transform duration-300 ${
isDropdownOpen ? 'rotate-180' : ''
}`} />
<ChevronDown
className={`h-4 w-4 transition-transform duration-300 ${
isDropdownOpen ? "rotate-180" : ""
}`}
/>
</button>
{/* Dropdown Menu */}
@@ -123,7 +148,9 @@ const Layout = ({ children }) => {
<div className="relative">
{TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
const categoryConfig = getCategoryConfig(
tool.category,
);
return (
<button
@@ -134,19 +161,23 @@ const Layout = ({ children }) => {
}}
className={`group flex items-center space-x-4 px-4 py-3 text-sm hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
: 'text-slate-700 dark:text-slate-300'
? "bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300"
: "text-slate-700 dark:text-slate-300"
}`}
>
<div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm group-hover:scale-110 transition-transform duration-300`}>
<div
className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm group-hover:scale-110 transition-transform duration-300`}
>
<IconComponent className="h-4 w-4 text-white" />
</div>
<div className="flex-1">
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-500 dark:text-slate-400">{tool.description}</div>
<div className="text-xs text-slate-600 dark:text-slate-600">
{tool.description}
</div>
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ChevronDown className="h-4 w-4 -rotate-90 text-slate-400" />
<ChevronDown className="h-4 w-4 -rotate-90 text-slate-600" />
</div>
</button>
);
@@ -164,8 +195,14 @@ const Layout = ({ children }) => {
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="md:hidden p-2 rounded-xl text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300"
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
aria-expanded={isMobileMenuOpen}
>
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
{isMobileMenuOpen ? (
<X className="h-6 w-6" />
) : (
<Menu className="h-6 w-6" />
)}
</button>
</div>
</div>
@@ -183,41 +220,11 @@ const Layout = ({ children }) => {
{/* Menu */}
<div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="space-y-2">
{/* Non-Tools Section */}
{NON_TOOLS.map((tool) => {
const IconComponent = tool.icon;
return (
<button
key={tool.path}
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<div className={`p-2 rounded-lg ${isActive(tool.path) ? 'bg-white/20' : 'bg-gradient-to-br from-indigo-500 to-purple-500'} shadow-sm`}>
<IconComponent className={`h-4 w-4 ${isActive(tool.path) ? 'text-white' : 'text-white'}`} />
</div>
<span>{tool.name}</span>
</button>
);
})}
<div className="border-t border-slate-200/50 dark:border-slate-700/50 pt-4 mt-4">
<div className="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider px-4 py-2 flex items-center gap-2">
<Sparkles className="h-3 w-3" />
{isToolPage ? 'Switch Tools' : 'Tools'}
</div>
{TOOLS.map((tool) => {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="space-y-2">
{/* Non-Tools Section */}
{NON_TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return (
<button
@@ -226,31 +233,69 @@ const Layout = ({ children }) => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
? "bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg"
: "text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50"
}`}
>
<div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm`}>
<IconComponent className="h-4 w-4 text-white" />
</div>
<div className="flex-1">
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-500 dark:text-slate-400">{tool.description}</div>
<div
className={`p-2 rounded-lg ${isActive(tool.path) ? "bg-white/20" : "bg-gradient-to-br from-indigo-500 to-purple-500"} shadow-sm`}
>
<IconComponent
className={`h-4 w-4 ${isActive(tool.path) ? "text-white" : "text-white"}`}
/>
</div>
<span>{tool.name}</span>
</button>
);
})}
<div className="border-t border-slate-200/50 dark:border-slate-700/50 pt-4 mt-4">
<div className="text-xs font-semibold text-slate-600 dark:text-slate-600 uppercase tracking-wider px-4 py-2 flex items-center gap-2">
<Sparkles className="h-3 w-3" />
{isToolPage ? "Switch Tools" : "Tools"}
</div>
{TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return (
<button
key={tool.path}
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? "bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300"
: "text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50"
}`}
>
<div
className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm`}
>
<IconComponent className="h-4 w-4 text-white" />
</div>
<div className="flex-1">
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-600 dark:text-slate-600">
{tool.description}
</div>
</div>
</button>
);
})}
</div>
</div>
</div>
</div>
</div>
</>
)}
{/* Main Content */}
<div className="flex flex-1 pt-16 min-w-0 w-full max-w-full overflow-x-hidden">
<div className="flex flex-1 pt-16 min-w-0 w-full max-w-full">
{/* Main Content Area */}
<main className="flex-1 flex flex-col min-w-0 w-full max-w-full">
{isToolPage && !isInvoicePreviewPage ? (
@@ -259,22 +304,18 @@ const Layout = ({ children }) => {
<ToolSidebar navigateWithGuard={navigateWithGuard} />
</div>
<div className="flex-1 flex flex-col pl-0 lg:pl-16 min-w-0">
<div className="p-4 sm:p-6 w-full min-w-0 max-w-full overflow-x-hidden">
<div className="p-4 sm:p-6 w-full min-w-0 max-w-full">
{children}
</div>
</div>
</div>
) : isInvoicePreviewPage ? (
<div className="flex-1 flex flex-col">
<div className="flex-1">
{children}
</div>
<div className="flex-1">{children}</div>
</div>
) : (
<div className="flex-1 flex flex-col">
<div className="flex-1">
{children}
</div>
<div className="flex-1">{children}</div>
{/* Global Footer for Homepage */}
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
@@ -287,11 +328,11 @@ const Layout = ({ children }) => {
src="/icon-192x192.png"
alt={SITE_CONFIG.title}
className="h-16 w-auto"
style={{ maxWidth: '100px' }}
style={{ maxWidth: "100px" }}
onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
e.target.style.display = "none";
e.target.nextSibling.style.display = "flex";
}}
/>
<div className="hidden items-center gap-3">
@@ -305,16 +346,16 @@ const Layout = ({ children }) => {
</div>
<div className="flex items-center justify-center gap-2 mb-3">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-sm font-medium text-slate-600 dark:text-slate-400">
<span className="text-sm font-medium text-slate-600 dark:text-slate-600">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-sm text-slate-500 dark:text-slate-500 mb-4">
<p className="text-sm text-slate-600 dark:text-slate-600 mb-4">
Built with for developers worldwide
</p>
<div className="flex flex-col items-center gap-4">
<div className="flex justify-center items-center gap-6 text-xs text-slate-400 dark:text-slate-500">
<div className="flex justify-center items-center gap-6 text-xs text-slate-600 dark:text-slate-600">
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
<span>100% Client-Side</span>
@@ -330,22 +371,26 @@ const Layout = ({ children }) => {
</div>
<div className="flex items-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
onClick={() => navigateWithGuard("/release-notes")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Release Notes
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<span className="text-slate-300 dark:text-slate-600">
</span>
<button
onClick={() => navigateWithGuard('/privacy')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
onClick={() => navigateWithGuard("/privacy")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<span className="text-slate-300 dark:text-slate-600">
</span>
<button
onClick={() => navigateWithGuard('/terms')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
onClick={() => navigateWithGuard("/terms")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</button>
@@ -369,39 +414,39 @@ const Layout = ({ children }) => {
src="/icon-192x192.png"
alt={SITE_CONFIG.title}
className="h-16 w-auto"
style={{ maxWidth: '100px' }}
style={{ maxWidth: "100px" }}
onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
e.target.style.display = "none";
e.target.nextSibling.style.display = "flex";
}}
/>
</div>
<div className="flex items-center justify-center gap-2 mb-2">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-xs font-medium text-slate-600 dark:text-slate-400">
<span className="text-xs font-medium text-slate-600 dark:text-slate-600">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<div className="flex items-center justify-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
onClick={() => navigateWithGuard("/release-notes")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Release Notes
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/privacy')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
onClick={() => navigateWithGuard("/privacy")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/terms')}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
onClick={() => navigateWithGuard("/terms")}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</button>

2
src/components/Loading.js Normal file → Executable file
View File

@@ -8,7 +8,7 @@ const Loading = () => {
<div className="absolute inset-0 bg-blue-500/20 blur-xl rounded-full animate-pulse"></div>
<Loader2 className="h-12 w-12 text-blue-600 dark:text-blue-400 animate-spin relative z-10" />
</div>
<p className="mt-4 text-slate-500 dark:text-slate-400 font-medium animate-pulse">
<p className="mt-4 text-slate-600 dark:text-slate-600 font-medium animate-pulse">
Loading...
</p>
</div>

18
src/components/MindmapView.js Normal file → Executable file
View File

@@ -521,7 +521,7 @@ const MindmapView = React.memo(({ data }) => {
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 transition-all duration-200 ease-in-out">
<div className="space-y-3">
<div>
<label className="text-xs text-gray-600 dark:text-gray-400 block mb-1">Edge Type</label>
<label className="text-xs text-gray-600 dark:text-gray-600 block mb-1">Edge Type</label>
<select
value={edgeType}
onChange={(e) => setEdgeType(e.target.value)}
@@ -535,7 +535,7 @@ const MindmapView = React.memo(({ data }) => {
</div>
<div>
<label className="text-xs text-gray-600 dark:text-gray-400 block mb-1">Line Color</label>
<label className="text-xs text-gray-600 dark:text-gray-600 block mb-1">Line Color</label>
<select
value={edgeColor}
onChange={(e) => setEdgeColor(e.target.value)}
@@ -558,7 +558,7 @@ const MindmapView = React.memo(({ data }) => {
onChange={(e) => setLayoutCompact(e.target.checked)}
className="text-xs"
/>
<label htmlFor="compact" className="text-xs text-gray-600 dark:text-gray-400">Compact Layout</label>
<label htmlFor="compact" className="text-xs text-gray-600 dark:text-gray-600">Compact Layout</label>
</div>
<div className="flex items-center space-x-2">
@@ -569,7 +569,7 @@ const MindmapView = React.memo(({ data }) => {
onChange={(e) => setSnapToGrid(e.target.checked)}
className="text-xs"
/>
<label htmlFor="snapToGrid" className="text-xs text-gray-600 dark:text-gray-400">Snap to Grid</label>
<label htmlFor="snapToGrid" className="text-xs text-gray-600 dark:text-gray-600">Snap to Grid</label>
</div>
</div>
</div>
@@ -597,31 +597,31 @@ const MindmapView = React.memo(({ data }) => {
<div className="w-4 h-4 bg-blue-100 border-2 border-blue-300 rounded flex items-center justify-center">
<Braces className="h-2.5 w-2.5 text-blue-600" />
</div>
<span className="text-xs text-gray-600 dark:text-gray-400">Object</span>
<span className="text-xs text-gray-600 dark:text-gray-600">Object</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-green-100 border-2 border-green-300 rounded flex items-center justify-center">
<List className="h-2.5 w-2.5 text-green-600" />
</div>
<span className="text-xs text-gray-600 dark:text-gray-400">Array</span>
<span className="text-xs text-gray-600 dark:text-gray-600">Array</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-purple-100 border-2 border-purple-300 rounded flex items-center justify-center">
<Type className="h-2.5 w-2.5 text-purple-600" />
</div>
<span className="text-xs text-gray-600 dark:text-gray-400">String</span>
<span className="text-xs text-gray-600 dark:text-gray-600">String</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-orange-100 border-2 border-orange-300 rounded flex items-center justify-center">
<Hash className="h-2.5 w-2.5 text-orange-600" />
</div>
<span className="text-xs text-gray-600 dark:text-gray-400">Number</span>
<span className="text-xs text-gray-600 dark:text-gray-600">Number</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-4 h-4 bg-yellow-100 border-2 border-yellow-300 rounded flex items-center justify-center">
<ToggleLeft className="h-2.5 w-2.5 text-yellow-600" />
</div>
<span className="text-xs text-gray-600 dark:text-gray-400">Boolean</span>
<span className="text-xs text-gray-600 dark:text-gray-600">Boolean</span>
</div>
</div>
</div>

57
src/components/MobileAdBanner.js Normal file → Executable file
View File

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

2
src/components/NavigationConfirmModal.js Normal file → Executable file
View File

@@ -73,7 +73,7 @@ const NavigationConfirmModal = ({ isOpen, onConfirm, onCancel, targetPath, hasDa
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
You currently have:
</p>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<ul className="text-sm text-gray-600 dark:text-gray-600 space-y-1">
{dataSummary.map((item, index) => (
<li key={index} className="flex items-center">
<span className="w-1.5 h-1.5 bg-amber-500 rounded-full mr-2 flex-shrink-0"></span>

18
src/components/OfferBlock.js Normal file → Executable file
View File

@@ -1,20 +1,16 @@
import React from 'react';
import React from "react";
const OfferBlock = () => {
return (
<div className="w-[300px] h-[250px] bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex flex-col items-center justify-center text-white p-6 text-center shadow-lg hover:shadow-xl transition-shadow duration-300">
<span className="bg-white/20 text-xs font-bold px-2 py-1 rounded mb-4 backdrop-blur-sm">
SPECIAL OFFER
<span className="bg-white/20 text-xs font-bold px-2 py-1 rounded mb-4 backdrop-blur-sm uppercase tracking-wider">
Coming Soon
</span>
<h3 className="text-2xl font-bold mb-2">
Upgrade to PRO
</h3>
<p className="text-indigo-100 text-sm mb-6">
Get unlimited access to all developer tools and features.
<h3 className="text-2xl font-bold mb-2">Upgrade to PRO</h3>
<p className="text-indigo-100 text-sm mb-6 leading-relaxed">
We are preparing a premium ad-free experience with exclusive developer
tools and features. Stay tuned!
</p>
<button className="bg-white text-indigo-600 font-bold py-2 px-6 rounded-full hover:bg-indigo-50 transition-colors shadow-md">
Learn More
</button>
</div>
);
};

22
src/components/PostmanTable.js Normal file → Executable file
View File

@@ -292,7 +292,7 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
<div className="flex items-center space-x-1 text-sm">
{getBreadcrumb().map((part, index) => (
<React.Fragment key={index}>
{index > 0 && <span className="text-gray-400 dark:text-gray-500">/</span>}
{index > 0 && <span className="text-gray-600 dark:text-gray-600">/</span>}
<button
onClick={() => handleBreadcrumbClick(index)}
className={`px-2 py-1 rounded transition-colors ${
@@ -309,14 +309,14 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-sm text-gray-500 dark:text-gray-400">
<div className="text-sm text-gray-600 dark:text-gray-600">
{isArrayView && `${currentData.length} items`}
{isObjectView && `${Object.keys(currentData).length} properties`}
</div>
{/* Global HTML/Raw Toggle */}
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-500 dark:text-gray-400">Text Display:</span>
<span className="text-xs text-gray-600 dark:text-gray-600">Text Display:</span>
<div className="flex rounded-md overflow-hidden border border-gray-300 dark:border-gray-600">
<button
onClick={() => setRenderHtml(true)}
@@ -353,11 +353,11 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-12">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wider w-12">
#
</th>
{headers.map(header => (
<th key={header} className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
<th key={header} className="px-4 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wider">
{header}
</th>
))}
@@ -372,7 +372,7 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
onClick={() => handleRowClick(index)}
className="hover:bg-blue-50 dark:hover:bg-blue-900/20 cursor-pointer transition-colors duration-150"
>
<td className="px-4 py-3 text-sm font-mono text-gray-500 dark:text-gray-400">
<td className="px-4 py-3 text-sm font-mono text-gray-600 dark:text-gray-600">
{index}
</td>
{isPrimitiveArray ? (
@@ -396,13 +396,13 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-1/3">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wider w-1/3">
Key
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wider">
Value
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-16">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wider w-16">
</th>
</tr>
@@ -438,7 +438,7 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
{copiedItems.has(`${currentPath.join('.')}.${key}`) ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3 text-gray-500 dark:text-gray-400" />
<Copy className="h-3 w-3 text-gray-600 dark:text-gray-600" />
)}
</button>
</td>
@@ -450,7 +450,7 @@ const PostmanTable = ({ data, title = "JSON Data" }) => {
) : (
// Fallback for primitive values
<div className="p-4">
<div className="text-center text-gray-500 dark:text-gray-400">
<div className="text-center text-gray-600 dark:text-gray-600">
<div className="text-lg font-mono text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words">
{formatFullValue(currentData)}
</div>

12
src/components/PostmanTreeTable.js Normal file → Executable file
View File

@@ -142,7 +142,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
<div className="flex items-center space-x-3">
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600 w-4 h-4" />
<input
type="text"
placeholder="Search keys or values..."
@@ -154,7 +154,7 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
{/* Type Filter */}
<div className="relative">
<Filter className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Filter className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-600 w-4 h-4" />
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
@@ -178,16 +178,16 @@ const PostmanTreeTable = ({ data, title = "JSON Data" }) => {
<table className="w-full">
<thead className="bg-gray-50 sticky top-0">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider">
Key
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider">
Type
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider">
Value
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-16">
<th className="px-4 py-3 text-left text-xs font-medium text-gray-600 uppercase tracking-wider w-16">
Actions
</th>
</tr>

2
src/components/ProBadge.js Normal file → Executable file
View File

@@ -110,7 +110,7 @@ export const ProFeatureLock = ({
</h4>
<ProBadge size="sm" />
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
<p className="text-sm text-gray-600 dark:text-gray-600 mb-3">
{featureDescription}
</p>
<ProBadge variant="button" size="md" onClick={handleUpgrade} />

4
src/components/RelatedTools.js Normal file → Executable file
View File

@@ -80,11 +80,11 @@ const RelatedTools = ({ toolId }) => {
<h4 className="font-medium text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{tool.name}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
<p className="text-sm text-gray-600 dark:text-gray-600 mt-1">
{tool.desc}
</p>
</div>
<ArrowRight className="h-5 w-5 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 group-hover:translate-x-1 transition-all flex-shrink-0 ml-2" />
<ArrowRight className="h-5 w-5 text-gray-600 group-hover:text-blue-600 dark:group-hover:text-blue-400 group-hover:translate-x-1 transition-all flex-shrink-0 ml-2" />
</div>
</Link>
))}

View File

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

0
src/components/SEO.js Normal file → Executable file
View File

0
src/components/SEOHead.js Normal file → Executable file
View File

585
src/components/StructuredEditor.js Normal file → Executable file
View File

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

View File

@@ -0,0 +1,30 @@
import React from "react";
import AdBlock from "./AdBlock";
import OfferBlock from "./OfferBlock";
import AffiliateBlock from "./AffiliateBlock";
const TabletAdSection = () => {
return (
<div className="hidden lg:flex xl:hidden flex-col mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-600 mb-4 text-center">
Sponsored
</h3>
<div className="flex justify-center gap-4 overflow-x-auto pb-4">
<div className="flex-shrink-0">
<AdBlock
adKey="7c55aebcdd74f6e9a8dc24bd13e7d949"
adDomain="downconvenientmagnetic.com"
/>
</div>
<div className="flex-shrink-0">
<OfferBlock />
</div>
<div className="flex-shrink-0">
<AffiliateBlock />
</div>
</div>
</div>
);
};
export default TabletAdSection;

0
src/components/ThemeToggle.js Normal file → Executable file
View File

6
src/components/ToolCard.js Normal file → Executable file
View File

@@ -45,7 +45,7 @@ const ToolCard = ({ icon: Icon, title, description, path, tags, category }) => {
return {
border: 'hover:border-slate-300 dark:hover:border-slate-500',
shadow: 'hover:shadow-slate-500/20',
titleColor: 'group-hover:text-slate-600 dark:group-hover:text-slate-400',
titleColor: 'group-hover:text-slate-600 dark:group-hover:text-slate-600',
arrowColor: 'group-hover:text-slate-600',
badgeColor: 'group-hover:bg-slate-100 dark:group-hover:bg-slate-700 group-hover:text-slate-700 dark:group-hover:text-slate-300'
};
@@ -67,7 +67,7 @@ const ToolCard = ({ icon: Icon, title, description, path, tags, category }) => {
<Icon className="h-6 w-6 text-white" />
</div>
<div className="flex-shrink-0 ml-4">
<ArrowRight className={`h-5 w-5 text-slate-400 ${hoverClasses.arrowColor} group-hover:translate-x-1 transition-all duration-300`} />
<ArrowRight className={`h-5 w-5 text-slate-600 ${hoverClasses.arrowColor} group-hover:translate-x-1 transition-all duration-300`} />
</div>
</div>
@@ -91,7 +91,7 @@ const ToolCard = ({ icon: Icon, title, description, path, tags, category }) => {
{tags.map((tag, index) => (
<span
key={index}
className="px-3 py-1 text-xs font-medium bg-slate-50 dark:bg-slate-700/50 text-slate-500 dark:text-slate-400 rounded-full border border-slate-200 dark:border-slate-600 group-hover:border-slate-300 dark:group-hover:border-slate-500 transition-colors"
className="px-3 py-1 text-xs font-medium bg-slate-50 dark:bg-slate-700/50 text-slate-600 dark:text-slate-600 rounded-full border border-slate-200 dark:border-slate-600 group-hover:border-slate-300 dark:group-hover:border-slate-500 transition-colors"
>
{tag}
</span>

8
src/components/ToolLayout.js Normal file → Executable file
View File

@@ -1,6 +1,7 @@
import React from 'react';
import AdColumn from './AdColumn';
import MobileAdBanner from './MobileAdBanner';
import TabletAdSection from './TabletAdSection';
const ToolLayout = ({ title, description, children, icon: Icon }) => {
return (
@@ -28,17 +29,16 @@ const ToolLayout = ({ title, description, children, icon: Icon }) => {
<div className="space-y-4 sm:space-y-6 w-full max-w-full min-w-0">
{children}
</div>
<TabletAdSection />
</div>
{/* Desktop Ad Column - Hidden on mobile */}
<AdColumn />
</div>
{/* Mobile Ad Banner - Hidden on desktop */}
<MobileAdBanner />
{/* Add padding to bottom on mobile to prevent content overlap with sticky ad */}
<div className="xl:hidden h-16" />
<div className="lg:hidden h-16" />
</>
);
};

37
src/components/ToolSidebar.js Normal file → Executable file
View File

@@ -107,11 +107,12 @@ const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="p-2 rounded-xl hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 group"
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{isCollapsed ? (
<ChevronRight className="h-4 w-4 text-slate-500 group-hover:text-blue-500 transition-colors" />
<ChevronRight className="h-4 w-4 text-slate-600 group-hover:text-blue-500 transition-colors" />
) : (
<ChevronLeft className="h-4 w-4 text-slate-500 group-hover:text-blue-500 transition-colors" />
<ChevronLeft className="h-4 w-4 text-slate-600 group-hover:text-blue-500 transition-colors" />
)}
</button>
</div>
@@ -121,7 +122,7 @@ const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
<div className="relative mt-4">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/10 to-purple-500/10 rounded-xl blur opacity-50"></div>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-400" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-600" />
<input
type="text"
placeholder="Search tools..."
@@ -176,7 +177,7 @@ const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
<IconComponent className={`${
isActiveItem
? 'h-5 w-5 text-white'
: 'h-4 w-4 text-slate-500 dark:text-slate-400 group-hover:text-white'
: 'h-4 w-4 text-slate-600 dark:text-slate-600 group-hover:text-white'
}`} />
</div>
) : (
@@ -193,7 +194,7 @@ const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
<IconComponent className={`h-4 w-4 ${
isActiveItem
? 'text-white'
: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
: 'text-slate-600 dark:text-slate-600 group-hover:text-white'
}`} />
</div>
<div className="flex-1 min-w-0">
@@ -203,8 +204,8 @@ const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
? 'text-amber-700 dark:text-amber-300'
: 'text-indigo-700 dark:text-indigo-300'
: isWhatsNew
? 'text-slate-500 dark:text-slate-400 group-hover:text-amber-600 dark:group-hover:text-amber-400'
: 'text-slate-500 dark:text-slate-400 group-hover:text-indigo-600 dark:group-hover:text-indigo-400'
? 'text-slate-600 dark:text-slate-600 group-hover:text-amber-600 dark:group-hover:text-amber-400'
: 'text-slate-600 dark:text-slate-600 group-hover:text-indigo-600 dark:group-hover:text-indigo-400'
}`}>
{tool.name}
{isWhatsNew && !isCollapsed && (
@@ -213,7 +214,7 @@ const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
</span>
)}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 truncate">
<div className="text-xs text-slate-600 dark:text-slate-600 truncate">
{tool.description}
</div>
</div>
@@ -238,12 +239,12 @@ const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
{/* Category Header */}
<button
onClick={() => toggleCategory(categoryKey)}
className="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors rounded-lg hover:bg-white/50 dark:hover:bg-slate-700/50"
className="w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-slate-600 dark:text-slate-600 hover:text-slate-800 dark:hover:text-slate-200 transition-colors rounded-lg hover:bg-white/50 dark:hover:bg-slate-700/50"
>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full bg-gradient-to-r ${categoryConfig.color}`}></div>
<span className="uppercase tracking-wider">{categoryConfig.name}</span>
<span className="text-slate-400 dark:text-slate-500">({tools.length})</span>
<span className="text-slate-600 dark:text-slate-600">({tools.length})</span>
</div>
{isExpanded ? (
<ChevronUp className="h-3 w-3" />
@@ -315,16 +316,16 @@ const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
<IconComponent className={`h-3.5 w-3.5 ${
isActiveItem
? 'text-white'
: 'text-slate-500 dark:text-slate-400 group-hover:text-white'
: 'text-slate-600 dark:text-slate-600 group-hover:text-white'
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium truncate text-sm ${
isActiveItem ? activeClasses.titleColor : 'text-slate-600 dark:text-slate-400 group-hover:text-slate-800 dark:group-hover:text-slate-200'
isActiveItem ? activeClasses.titleColor : 'text-slate-600 dark:text-slate-600 group-hover:text-slate-800 dark:group-hover:text-slate-200'
}`}>
{tool.name}
</div>
<div className="text-xs text-slate-500 dark:text-slate-500 truncate">
<div className="text-xs text-slate-600 dark:text-slate-600 truncate">
{tool.description}
</div>
</div>
@@ -390,12 +391,12 @@ const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
: `border border-gray-300 dark:border-slate-600 bg-transparent`
}`}>
<IconComponent className={`h-3.5 w-3.5 ${
isActiveItem ? 'text-white' : 'text-gray-500 dark:text-gray-400'
isActiveItem ? 'text-white' : 'text-gray-600 dark:text-gray-600'
}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`font-medium text-sm truncate ${isActiveItem ? `text-white` : `text-gray-500 dark:text-gray-400`}`}>{tool.name}</div>
<div className={`text-xs ${isActiveItem ? `text-white` : `text-gray-500 dark:text-gray-400`} truncate`}>{tool.description}</div>
<div className={`font-medium text-sm truncate ${isActiveItem ? `text-white` : `text-gray-600 dark:text-gray-600`}`}>{tool.name}</div>
<div className={`text-xs ${isActiveItem ? `text-white` : `text-gray-600 dark:text-gray-600`} truncate`}>{tool.description}</div>
</div>
</a>
);
@@ -415,12 +416,12 @@ const ToolSidebar = ({ navigateWithGuard: propNavigateWithGuard }) => {
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<div className="w-1.5 h-1.5 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full animate-pulse"></div>
<span className="text-xs font-medium text-slate-500 dark:text-slate-400">
<span className="text-xs font-medium text-slate-600 dark:text-slate-600">
Quick Access
</span>
<div className="w-1.5 h-1.5 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-xs text-slate-400 dark:text-slate-500">
<p className="text-xs text-slate-600 dark:text-slate-600">
{SITE_CONFIG.totalTools} tools available
</p>
</div>

0
src/components/invoice-templates/MinimalTemplate.js Normal file → Executable file
View File

0
src/config/features.js Normal file → Executable file
View File

216
src/config/tools.js Normal file → Executable file
View File

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

0
src/data/faqs.js Normal file → Executable file
View File

0
src/hooks/useAnalytics.js Normal file → Executable file
View File

0
src/hooks/useNavigationGuard.js Normal file → Executable file
View File

104
src/index.css Normal file → Executable file
View File

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

0
src/index.js Normal file → Executable file
View File

8
src/pages/Base64Tool.js Normal file → Executable file
View File

@@ -85,7 +85,7 @@ const Base64Tool = () => {
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'encode'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
: 'text-gray-600 dark:text-gray-600'
}`}
>
Encode
@@ -95,7 +95,7 @@ const Base64Tool = () => {
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'decode'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
: 'text-gray-600 dark:text-gray-600'
}`}
>
Decode
@@ -147,7 +147,7 @@ const Base64Tool = () => {
</div>
{/* Output */}
<div className="space-y-2">
<div className="space-y-2" aria-live="polite">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'encode' ? 'Base64 Output' : 'Decoded Text'}
</label>
@@ -160,7 +160,7 @@ const Base64Tool = () => {
? 'Base64 encoded text will appear here...'
: 'Decoded text will appear here...'
}
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
className={`tool-input h-96 bg-gray-50 dark:bg-gray-800 ${output?.startsWith('Error:') ? 'border-red-300 dark:border-red-700' : ''}`}
/>
{output && <CopyButton text={output} />}
</div>

8
src/pages/BeautifierTool.js Normal file → Executable file
View File

@@ -366,7 +366,7 @@ const BeautifierTool = () => {
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'beautify'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
: 'text-gray-600 dark:text-gray-600'
}`}
>
Beautify
@@ -376,7 +376,7 @@ const BeautifierTool = () => {
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'minify'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
: 'text-gray-600 dark:text-gray-600'
}`}
>
Minify
@@ -415,7 +415,7 @@ const BeautifierTool = () => {
</div>
{/* Output */}
<div className="space-y-2">
<div className="space-y-2" aria-live="polite">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'beautify' ? 'Beautified' : 'Minified'} Output
</label>
@@ -424,7 +424,7 @@ const BeautifierTool = () => {
value={output}
readOnly
placeholder={`${mode === 'beautify' ? 'Beautified' : 'Minified'} code will appear here...`}
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
className={`tool-input h-96 bg-gray-50 dark:bg-gray-800 ${output?.startsWith('Error:') ? 'border-red-300 dark:border-red-700' : ''}`}
/>
{output && <CopyButton text={output} />}
</div>

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

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

12
src/pages/DiffTool.js Normal file → Executable file
View File

@@ -142,7 +142,7 @@ const user = {
className={`px-4 py-2 rounded-md font-medium transition-colors ${
diffMode === 'unified'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
: 'text-gray-600 dark:text-gray-600'
}`}
>
Unified Diff
@@ -152,7 +152,7 @@ const user = {
className={`px-4 py-2 rounded-md font-medium transition-colors ${
diffMode === 'split'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
: 'text-gray-600 dark:text-gray-600'
}`}
>
Side by Side
@@ -267,7 +267,7 @@ const user = {
</div>
) : (
<div className="flex items-center justify-center h-32 bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg">
<p className="text-gray-500 dark:text-gray-400">Enter text in both fields to see the comparison</p>
<p className="text-gray-600 dark:text-gray-600">Enter text in both fields to see the comparison</p>
</div>
)}
</div>
@@ -279,15 +279,15 @@ const user = {
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div className="flex items-center space-x-2">
<span className="font-mono text-red-600">- line</span>
<span className="text-gray-600 dark:text-gray-400">Removed from Text A</span>
<span className="text-gray-600 dark:text-gray-600">Removed from Text A</span>
</div>
<div className="flex items-center space-x-2">
<span className="font-mono text-green-600">+ line</span>
<span className="text-gray-600 dark:text-gray-400">Added in Text B</span>
<span className="text-gray-600 dark:text-gray-600">Added in Text B</span>
</div>
<div className="flex items-center space-x-2">
<span className="font-mono text-gray-600"> line</span>
<span className="text-gray-600 dark:text-gray-400">Unchanged</span>
<span className="text-gray-600 dark:text-gray-600">Unchanged</span>
</div>
</div>
</div>

10
src/pages/Home.js Normal file → Executable file
View File

@@ -58,7 +58,7 @@ const Home = () => {
{SITE_CONFIG.subtitle}
</p>
<p className="text-lg text-slate-500 dark:text-slate-400 mb-12 max-w-2xl mx-auto">
<p className="text-lg text-slate-600 dark:text-slate-600 mb-12 max-w-2xl mx-auto">
{SITE_CONFIG.slogan} {SITE_CONFIG.description}
</p>
@@ -66,7 +66,7 @@ const Home = () => {
<div className="relative max-w-lg mx-auto mb-8">
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl blur opacity-20"></div>
<div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-400" />
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-slate-600" />
<input
type="text"
placeholder="Search tools..."
@@ -78,7 +78,7 @@ const Home = () => {
</div>
{/* Stats */}
<div className="flex flex-col sm:flex-row justify-center items-center gap-4 sm:gap-8 text-sm text-slate-500 dark:text-slate-400 mb-8">
<div className="flex flex-col sm:flex-row justify-center items-center gap-4 sm:gap-8 text-sm text-slate-600 dark:text-slate-600 mb-8">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span>{SITE_CONFIG.totalTools} Tools Available</span>
@@ -151,10 +151,10 @@ const Home = () => {
{filteredTools.length === 0 && (
<div className="text-center py-20">
<div className="text-6xl mb-4">🔍</div>
<p className="text-slate-500 dark:text-slate-400 text-xl mb-2">
<p className="text-slate-600 dark:text-slate-600 text-xl mb-2">
No tools found matching "{searchTerm}"
</p>
<p className="text-slate-400 dark:text-slate-500">
<p className="text-slate-600 dark:text-slate-600">
Try searching for "editor", "encode", or "format"
</p>
</div>

114
src/pages/InvoiceEditor.js Normal file → Executable file
View File

@@ -777,7 +777,7 @@ const InvoiceEditor = () => {
className={`${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
: 'border-transparent text-gray-600 hover:text-gray-700 hover:border-gray-300 dark:text-gray-600 dark:hover:text-gray-300'
} whitespace-nowrap py-4 px-1 sm:px-2 border-b-2 font-medium text-sm flex items-center gap-1 sm:gap-2 transition-colors min-w-0 flex-shrink-0`}
>
<Icon className="h-4 w-4" />
@@ -793,11 +793,11 @@ const InvoiceEditor = () => {
<div className="p-4">
{activeTab === 'create' && (
<div className="text-center py-12">
<FileText className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500 mb-4" />
<FileText className="mx-auto h-12 w-12 text-gray-600 dark:text-gray-600 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Start Building Your Invoice
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-8 max-w-md mx-auto">
<p className="text-gray-600 dark:text-gray-600 mb-8 max-w-md mx-auto">
Choose how you'd like to begin creating your professional invoice
</p>
@@ -813,11 +813,11 @@ const InvoiceEditor = () => {
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors group"
>
<Plus className="h-8 w-8 text-gray-400 group-hover:text-blue-500 dark:group-hover:text-blue-400 mb-2" />
<Plus className="h-8 w-8 text-gray-600 group-hover:text-blue-500 dark:group-hover:text-blue-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400">
Start Empty
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
<span className="text-xs text-gray-600 dark:text-gray-600 text-center mt-1">
Create a blank invoice
</span>
</button>
@@ -833,11 +833,11 @@ const InvoiceEditor = () => {
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-green-500 dark:hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors group"
>
<FileText className="h-8 w-8 text-gray-400 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
<FileText className="h-8 w-8 text-gray-600 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-green-600 dark:group-hover:text-green-400">
Load Sample
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
<span className="text-xs text-gray-600 dark:text-gray-600 text-center mt-1">
Start with example invoice
</span>
</button>
@@ -866,7 +866,7 @@ const InvoiceEditor = () => {
{isLoading ? 'Fetching...' : 'Fetch Data'}
</button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-xs text-gray-600 dark:text-gray-600">
Enter any URL that returns exported JSON data from your previous invoice work.
</p>
</div>
@@ -908,7 +908,7 @@ const InvoiceEditor = () => {
</div>
)}
<div className="flex items-center justify-between flex-shrink-0">
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-sm text-gray-600 dark:text-gray-600">
Supports JSON invoice templates
</div>
<button
@@ -985,9 +985,9 @@ const InvoiceEditor = () => {
<div className="p-4 sm:p-6 space-y-4 sm:space-y-6">
{!createNewCompleted ? (
<div className="text-center py-12">
<FileText className="mx-auto h-12 w-12 text-gray-400 mb-4" />
<FileText className="mx-auto h-12 w-12 text-gray-600 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No Invoice Data Loaded</h3>
<p className="text-gray-500 dark:text-gray-400">
<p className="text-gray-600 dark:text-gray-600">
Use the input section above to create a new invoice or load existing data.
</p>
</div>
@@ -1080,7 +1080,7 @@ const InvoiceEditor = () => {
'--tw-ring-color': `${invoiceData.settings?.colorScheme || '#3B82F6'}40`
}}
/>
<span className="text-xs text-gray-500 dark:text-gray-400">Show in preview</span>
<span className="text-xs text-gray-600 dark:text-gray-600">Show in preview</span>
</label>
</div>
@@ -1121,7 +1121,7 @@ const InvoiceEditor = () => {
className="flex-1 text-sm text-gray-600 dark:text-gray-300 bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-blue-500 focus:outline-none focus:ring-0 pb-1 disabled:cursor-not-allowed"
placeholder="Phone"
/>
<span className="text-gray-400 text-sm self-end pb-1 hidden sm:block">|</span>
<span className="text-gray-600 text-sm self-end pb-1 hidden sm:block">|</span>
<input
type="email"
value={invoiceData.company.email}
@@ -1173,7 +1173,7 @@ const InvoiceEditor = () => {
className="flex-1 text-sm text-gray-600 dark:text-gray-300 bg-transparent border-0 border-b border-gray-200 dark:border-gray-600 focus:border-green-500 focus:outline-none focus:ring-0 pb-1"
placeholder="Phone"
/>
<span className="text-gray-400 text-sm self-end pb-1 hidden sm:block">|</span>
<span className="text-gray-600 text-sm self-end pb-1 hidden sm:block">|</span>
<input
type="email"
value={invoiceData.client.email}
@@ -1233,7 +1233,7 @@ const InvoiceEditor = () => {
</td>
<td className="px-3 py-3 text-center" style={{ width: 'auto' }}>
<div className="relative flex justify-end items-center">
<span className="text-gray-500 dark:text-gray-400 px-2 py-1 text-xs rounded-1 bg-gray-100 dark:bg-gray-900/20">{invoiceData.settings?.currency?.symbol || '$'}</span>
<span className="text-gray-600 dark:text-gray-600 px-2 py-1 text-xs rounded-1 bg-gray-100 dark:bg-gray-900/20">{invoiceData.settings?.currency?.symbol || '$'}</span>
<input
type="text"
value={formatNumber(item.rate)}
@@ -1264,7 +1264,7 @@ const InvoiceEditor = () => {
<button
onClick={() => moveItem('items', item.id, 'up')}
disabled={index === 0}
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
className="p-1 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move up"
>
<ChevronUp className="h-4 w-4" />
@@ -1272,7 +1272,7 @@ const InvoiceEditor = () => {
<button
onClick={() => moveItem('items', item.id, 'down')}
disabled={index === invoiceData.items.length - 1}
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
className="p-1 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move down"
>
<ChevronDown className="h-4 w-4" />
@@ -1372,7 +1372,7 @@ const InvoiceEditor = () => {
<button
onClick={() => moveItem('fees', fee.id, 'up')}
disabled={index === 0}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
className="p-2 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move up"
>
<ChevronUp className="h-4 w-4" />
@@ -1380,7 +1380,7 @@ const InvoiceEditor = () => {
<button
onClick={() => moveItem('fees', fee.id, 'down')}
disabled={index === (invoiceData.fees || []).length - 1}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
className="p-2 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move down"
>
<ChevronDown className="h-4 w-4" />
@@ -1449,7 +1449,7 @@ const InvoiceEditor = () => {
<button
onClick={() => moveItem('discounts', discount.id, 'up')}
disabled={index === 0}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
className="p-2 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move up"
>
<ChevronUp className="h-4 w-4" />
@@ -1457,7 +1457,7 @@ const InvoiceEditor = () => {
<button
onClick={() => moveItem('discounts', discount.id, 'down')}
disabled={index === (invoiceData.discounts || []).length - 1}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
className="p-2 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move down"
>
<ChevronDown className="h-4 w-4" />
@@ -1484,14 +1484,14 @@ const InvoiceEditor = () => {
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center py-2 border-b border-emerald-200 dark:border-emerald-700">
<span className="text-gray-600 dark:text-gray-400">Subtotal:</span>
<span className="text-gray-600 dark:text-gray-600">Subtotal:</span>
<span className="font-medium text-gray-900 dark:text-white flex-shrink-0">{formatCurrency(invoiceData.subtotal, true)}</span>
</div>
{/* Dynamic Fees */}
{(invoiceData.fees || []).map((fee) => (
<div key={fee.id} className="flex justify-between items-center py-2 border-b border-emerald-200 dark:border-emerald-700">
<span className="text-gray-600 dark:text-gray-400">
<span className="text-gray-600 dark:text-gray-600">
{fee.label || 'Fee'} {fee.type === 'percentage' ? `(${fee.value}%)` : ''}:
</span>
<span className="font-medium text-blue-600 dark:text-blue-400 flex-shrink-0">
@@ -1503,7 +1503,7 @@ const InvoiceEditor = () => {
{/* Dynamic Discounts */}
{(invoiceData.discounts || []).map((discount) => (
<div key={discount.id} className="flex justify-between items-center py-2 border-b border-emerald-200 dark:border-emerald-700">
<span className="text-gray-600 dark:text-gray-400">
<span className="text-gray-600 dark:text-gray-600">
{discount.label || 'Discount'} {discount.type === 'percentage' ? `(${discount.value}%)` : ''}:
</span>
<span className="font-medium text-red-600 dark:text-red-400 flex-shrink-0">
@@ -1515,7 +1515,7 @@ const InvoiceEditor = () => {
{/* Legacy Discount */}
{invoiceData.discount > 0 && (
<div className="flex justify-between items-center py-2 border-b border-emerald-200 dark:border-emerald-700">
<span className="text-gray-600 dark:text-gray-400">Discount:</span>
<span className="text-gray-600 dark:text-gray-600">Discount:</span>
<span className="font-medium text-red-600 dark:text-red-400 flex-shrink-0">-{formatCurrency(invoiceData.discount, true)}</span>
</div>
)}
@@ -1819,7 +1819,7 @@ const InvoiceEditor = () => {
<button
onClick={() => moveInstallment(installment.id, 'up')}
disabled={index === 0}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
className="p-2 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move up"
>
<ChevronUp className="h-4 w-4" />
@@ -1827,7 +1827,7 @@ const InvoiceEditor = () => {
<button
onClick={() => moveInstallment(installment.id, 'down')}
disabled={index === (invoiceData.paymentTerms?.installments || []).length - 1}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
className="p-2 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
title="Move down"
>
<ChevronDown className="h-4 w-4" />
@@ -1956,7 +1956,7 @@ const InvoiceEditor = () => {
onChange={handleSignatureUpload}
className="hidden"
/>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
<p className="text-xs text-gray-600 dark:text-gray-600 mt-1">
Upload an image of your signature (PNG, JPG recommended)
</p>
</div>
@@ -2227,7 +2227,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Invoice Settings</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
className="text-gray-600 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
@@ -2242,7 +2242,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
className={`px-6 py-3 text-sm font-medium relative transition-all duration-200 ${
activeTab === 'general'
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
: 'text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
>
General
@@ -2255,7 +2255,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
className={`px-6 py-3 text-sm font-medium relative transition-all duration-200 ${
activeTab === 'layout'
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
: 'text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
>
Layout
@@ -2268,7 +2268,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
className={`px-6 py-3 text-sm font-medium relative transition-all duration-200 ${
activeTab === 'payment'
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
: 'text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50'
}`}
>
Payment
@@ -2335,10 +2335,10 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
className="w-12 h-12 rounded-lg border border-gray-300 cursor-pointer bg-transparent"
/>
<div>
<p className="text-sm text-gray-600 dark:text-gray-400">
<p className="text-sm text-gray-600 dark:text-gray-600">
This color will be used throughout the invoice and PDF
</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
<p className="text-xs text-gray-600 dark:text-gray-600 mt-1">
Current: {invoiceData.settings?.colorScheme || '#3B82F6'}
</p>
</div>
@@ -2355,7 +2355,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
selectedCurrency={invoiceData.settings?.currency}
onSelect={(currency) => onUpdateSettings('currency', currency)}
/>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
<p className="text-xs text-gray-600 dark:text-gray-600 mt-1">
Selected: {invoiceData.settings?.currency?.symbol || invoiceData.settings?.currency?.code || '$'} ({invoiceData.settings?.currency?.code || 'USD'})
</p>
</div>
@@ -2373,7 +2373,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
Use Thousand Separator
</span>
<p className="text-xs text-gray-500 dark:text-gray-500">
<p className="text-xs text-gray-600 dark:text-gray-600">
Format numbers like 1,000.00 instead of 1000.00
</p>
</div>
@@ -2395,7 +2395,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
<option value={2}>2 (1000.00)</option>
<option value={3}>3 (1000.000)</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
<p className="text-xs text-gray-600 dark:text-gray-600 mt-1">
Number of decimal places to display
</p>
</div>
@@ -2420,7 +2420,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
<option value="normal">Normal (25px spacing)</option>
<option value="spacious">Spacious (40px spacing)</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p className="text-xs text-gray-600 dark:text-gray-600 mt-1">
Controls the spacing between major sections for better multi-page layout
</p>
</div>
@@ -2468,7 +2468,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
<span className="text-sm text-gray-700 dark:text-gray-300">Force page break before Payment Method</span>
</label>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
<p className="text-xs text-gray-600 dark:text-gray-600 mt-2">
Use page breaks to ensure important sections start on a new page in PDF output
</p>
</div>
@@ -2493,7 +2493,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
<option value="link">Payment Link</option>
<option value="qr">QR Code</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
<p className="text-xs text-gray-600 dark:text-gray-600 mt-1">
Choose how payment information appears on your invoice
</p>
</div>
@@ -2505,7 +2505,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">Bank Details</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
Bank Name
</label>
<input
@@ -2517,7 +2517,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
Account Name
</label>
<input
@@ -2529,7 +2529,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
Account Number
</label>
<input
@@ -2541,7 +2541,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
Routing Number
</label>
<input
@@ -2553,7 +2553,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
SWIFT Code
</label>
<input
@@ -2565,7 +2565,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
IBAN
</label>
<input
@@ -2586,7 +2586,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">Payment Link</h4>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
Payment URL
</label>
<input
@@ -2598,7 +2598,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
Button Label
</label>
<input
@@ -2620,7 +2620,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
{/* QR Code Type Selection */}
<div className="mb-3">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-2">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-2">
QR Code Type
</label>
<div className="flex gap-4">
@@ -2653,7 +2653,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
{invoiceData.paymentMethod?.qrCode?.customImage === undefined && (
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
Payment URL
</label>
<input
@@ -2664,7 +2664,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
placeholder="https://pay.stripe.com/..."
/>
</div>
<p className="text-xs text-gray-500 dark:text-gray-500">
<p className="text-xs text-gray-600 dark:text-gray-600">
QR code will be automatically generated from this URL
</p>
</div>
@@ -2674,7 +2674,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
{invoiceData.paymentMethod?.qrCode?.customImage !== undefined && (
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
Upload QR Code Image
</label>
<div className="flex items-center gap-3">
@@ -2724,7 +2724,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
{/* Common QR Code Label */}
<div className="mt-3">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
<label className="block text-xs font-medium text-gray-600 dark:text-gray-600 mb-1">
QR Code Label
</label>
<input
@@ -2755,7 +2755,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
<option value="OVERDUE">OVERDUE</option>
<option value="PENDING">PENDING</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
<p className="text-xs text-gray-600 dark:text-gray-600 mt-1">
Add a status stamp to your invoice PDF
</p>
</div>
@@ -2772,7 +2772,7 @@ const InvoiceSettingsModal = ({ invoiceData, currencies, onUpdateSettings, onClo
onChange={(e) => onUpdateSettings('paymentDate', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
/>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
<p className="text-xs text-gray-600 dark:text-gray-600 mt-1">
Date when payment was received
</p>
</div>
@@ -2836,7 +2836,7 @@ const InputChangeConfirmationModal = ({ invoiceData, currentMethod, newMethod, o
<div className="px-6 py-4">
<div className="space-y-3">
<p className="text-sm text-gray-600 dark:text-gray-400">
<p className="text-sm text-gray-600 dark:text-gray-600">
You currently have:
</p>
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-1 ml-4">
@@ -2946,7 +2946,7 @@ const SignaturePadModal = ({ isOpen, onClose, onSave }) => {
/>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
<p className="text-sm text-gray-600 dark:text-gray-600 mb-4">
Draw your signature in the box above using your mouse or touch device.
</p>

2
src/pages/InvoicePreview.js Normal file → Executable file
View File

@@ -335,7 +335,7 @@ const InvoicePreview = () => {
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Invoice Preview</h2>
<div className="hidden sm:flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<div className="hidden sm:flex items-center gap-2 text-sm text-gray-600 dark:text-gray-600">
<span></span>
<span>{pdfPageSize} Format</span>
</div>

2
src/pages/InvoicePreviewMinimal.js Normal file → Executable file
View File

@@ -217,7 +217,7 @@ const InvoicePreviewMinimal = () => {
<div className="flex items-center gap-2 text-sm text-gray-600">
<FileText className="h-4 w-4" />
<span className="font-medium">Minimal Invoice Preview</span>
<span className="text-gray-400"></span>
<span className="text-gray-600"></span>
<span>{pdfPageSize} Format</span>
</div>
</div>

1847
src/pages/MarkdownEditor.js Normal file → Executable file

File diff suppressed because it is too large Load Diff

8
src/pages/NotFound.js Normal file → Executable file
View File

@@ -26,7 +26,7 @@ const NotFound = () => {
404
</h1>
<div className="mt-4 flex items-center justify-center gap-2">
<Search className="h-6 w-6 text-gray-400" />
<Search className="h-6 w-6 text-gray-600" />
<p className="text-2xl font-semibold text-gray-700 dark:text-gray-300">
Page Not Found
</p>
@@ -34,7 +34,7 @@ const NotFound = () => {
</div>
{/* Message */}
<p className="text-lg text-gray-600 dark:text-gray-400 mb-12">
<p className="text-lg text-gray-600 dark:text-gray-600 mb-12">
Oops! The page you're looking for doesn't exist. It might have been moved or deleted.
</p>
@@ -60,7 +60,7 @@ const NotFound = () => {
<h3 className="font-semibold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{tool.name}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
<p className="text-sm text-gray-600 dark:text-gray-600 mt-1">
{tool.desc}
</p>
</div>
@@ -81,7 +81,7 @@ const NotFound = () => {
</Link>
{/* Search Suggestion */}
<p className="mt-8 text-sm text-gray-500 dark:text-gray-400">
<p className="mt-8 text-sm text-gray-600 dark:text-gray-600">
Or use the search bar at the top to find what you need
</p>
</div>

40
src/pages/ObjectEditor.js Normal file → Executable file
View File

@@ -773,7 +773,7 @@ const ObjectEditor = () => {
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 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Plus className="h-4 w-4 flex-shrink-0" />
@@ -785,7 +785,7 @@ const ObjectEditor = () => {
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 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Globe className="h-4 w-4 flex-shrink-0" />
@@ -796,7 +796,7 @@ const ObjectEditor = () => {
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 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<FileText className="h-4 w-4 flex-shrink-0" />
@@ -807,7 +807,7 @@ const ObjectEditor = () => {
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 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Upload className="h-4 w-4 flex-shrink-0" />
@@ -826,7 +826,7 @@ const ObjectEditor = () => {
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Create New Object
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
<p className="text-sm text-gray-600 dark:text-gray-600 mb-4">
Choose how you'd like to begin working with your data
</p>
</div>
@@ -846,11 +846,11 @@ const ObjectEditor = () => {
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors group"
>
<Plus className="h-8 w-8 text-gray-400 group-hover:text-blue-500 dark:group-hover:text-blue-400 mb-2" />
<Plus className="h-8 w-8 text-gray-600 group-hover:text-blue-500 dark:group-hover:text-blue-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400">
Start Empty
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
<span className="text-xs text-gray-600 dark:text-gray-600 text-center mt-1">
Create a blank object structure
</span>
</button>
@@ -880,11 +880,11 @@ const ObjectEditor = () => {
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-green-500 dark:hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors group"
>
<FileText className="h-8 w-8 text-gray-400 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
<FileText className="h-8 w-8 text-gray-600 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-green-600 dark:group-hover:text-green-400">
Load Sample
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
<span className="text-xs text-gray-600 dark:text-gray-600 text-center mt-1">
Start with example data to explore features
</span>
</button>
@@ -909,7 +909,7 @@ const ObjectEditor = () => {
✓ Data loaded: {urlDataSummary.format} ({urlDataSummary.size.toLocaleString()} chars, {urlDataSummary.properties} {urlDataSummary.properties === 1 ? 'property' : 'properties'})
</span>
{urlDataSummary.contentTypeLabel && (
<span className="text-xs text-gray-600 dark:text-gray-400">
<span className="text-xs text-gray-600 dark:text-gray-600">
{urlDataSummary.contentTypeEmoji} {urlDataSummary.contentTypeLabel}
</span>
)}
@@ -976,7 +976,7 @@ const ObjectEditor = () => {
</div>
)}
<div className="flex items-center justify-between flex-shrink-0">
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-sm text-gray-600 dark:text-gray-600">
{inputFormat && (
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${
inputValid
@@ -1057,7 +1057,7 @@ const ObjectEditor = () => {
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors ${
viewMode === 'visual'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
<Edit3 className="h-4 w-4" />
@@ -1068,7 +1068,7 @@ const ObjectEditor = () => {
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${
viewMode === 'mindmap'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
<Workflow className="h-4 w-4" />
@@ -1079,7 +1079,7 @@ const ObjectEditor = () => {
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 ${
viewMode === 'table'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
<Table className="h-4 w-4" />
@@ -1093,11 +1093,11 @@ const ObjectEditor = () => {
<div>
{Object.keys(structuredData).length === 0 ? (
<div className="text-center py-12">
<Edit3 className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
<Edit3 className="h-12 w-12 text-gray-600 dark:text-gray-600 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
No Object Data
</h3>
<p className="text-gray-600 dark:text-gray-400">
<p className="text-gray-600 dark:text-gray-600">
Load data using the input methods above to start editing
</p>
</div>
@@ -1143,7 +1143,7 @@ const ObjectEditor = () => {
Export Results
{outputExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</h3>
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-sm text-gray-600 dark:text-gray-600">
<span>Object: {Object.keys(structuredData).length} properties</span>
</div>
</div>
@@ -1159,7 +1159,7 @@ const ObjectEditor = () => {
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
activeExportTab === 'json'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Braces className="h-4 w-4" />
@@ -1170,7 +1170,7 @@ const ObjectEditor = () => {
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
activeExportTab === 'php'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<Code className="h-4 w-4" />
@@ -1428,7 +1428,7 @@ const InputChangeConfirmationModal = ({ objectData, currentMethod, newMethod, on
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
This will permanently delete:
</h4>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<ul className="text-sm text-gray-600 dark:text-gray-600 space-y-1">
<li> Object with {objectSize} properties</li>
{hasNestedData && <li> All nested objects and arrays</li>}
<li> All modifications and edits</li>

2
src/pages/PrivacyPolicy.js Normal file → Executable file
View File

@@ -252,7 +252,7 @@ const PrivacyPolicy = () => {
{/* Footer */}
<div className="mt-8 text-center">
<p className="text-sm text-slate-500 dark:text-slate-400">
<p className="text-sm text-slate-600 dark:text-slate-600">
© {SITE_CONFIG.year} {SITE_CONFIG.title} Your privacy is our priority
</p>
</div>

16
src/pages/ReleaseNotes.js Normal file → Executable file
View File

@@ -235,7 +235,7 @@ const ReleaseNotes = () => {
<h1 className="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-4">
What's New
</h1>
<p className="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
<p className="text-xl text-gray-600 dark:text-gray-600 max-w-2xl mx-auto">
Discover the latest features, improvements, and bug fixes that make your development workflow even better.
</p>
</div>
@@ -269,7 +269,7 @@ const ReleaseNotes = () => {
className="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div className="flex items-center space-x-3">
<Calendar className="h-5 w-5 text-gray-500 dark:text-gray-400" />
<Calendar className="h-5 w-5 text-gray-600 dark:text-gray-600" />
<div className="text-left">
<h3 className="font-semibold text-gray-900 dark:text-gray-100">
{releaseDate.toLocaleDateString('en-US', {
@@ -279,16 +279,16 @@ const ReleaseNotes = () => {
day: 'numeric'
})}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="text-sm text-gray-600 dark:text-gray-600">
{dayReleases.length} update{dayReleases.length !== 1 ? 's' : ''}
{isRecent && <span className="ml-2 text-blue-600 dark:text-blue-400">• Recent</span>}
</p>
</div>
</div>
{isExpanded ? (
<ChevronUp className="h-5 w-5 text-gray-400" />
<ChevronUp className="h-5 w-5 text-gray-600" />
) : (
<ChevronDown className="h-5 w-5 text-gray-400" />
<ChevronDown className="h-5 w-5 text-gray-600" />
)}
</button>
@@ -318,10 +318,10 @@ const ReleaseNotes = () => {
{typeConfig.label}
</span>
</div>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
<p className="text-gray-600 dark:text-gray-600 leading-relaxed">
{release.description}
</p>
<div className="mt-3 flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
<div className="mt-3 flex items-center space-x-4 text-xs text-gray-600 dark:text-gray-600">
<span>
{new Date(release.date).toLocaleTimeString('en-US', {
hour: '2-digit',
@@ -346,7 +346,7 @@ const ReleaseNotes = () => {
{/* Footer */}
<div className="text-center mt-12 py-8 border-t border-gray-200 dark:border-gray-700">
<p className="text-gray-500 dark:text-gray-400">
<p className="text-gray-600 dark:text-gray-600">
Stay tuned for more exciting updates and improvements!
</p>
</div>

78
src/pages/TableEditor.js Normal file → Executable file
View File

@@ -1863,7 +1863,7 @@ const TableEditor = () => {
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 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
: "text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200"
}`}
>
<Plus className="h-4 w-4" />
@@ -1874,7 +1874,7 @@ const TableEditor = () => {
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 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
: "text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200"
}`}
>
<Globe className="h-4 w-4" />
@@ -1885,7 +1885,7 @@ const TableEditor = () => {
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 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
: "text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200"
}`}
>
<FileText className="h-4 w-4" />
@@ -1896,7 +1896,7 @@ const TableEditor = () => {
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 === "upload"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
: "text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200"
}`}
>
<Upload className="h-4 w-4" />
@@ -1914,7 +1914,7 @@ const TableEditor = () => {
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Start Building Your Table
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
<p className="text-sm text-gray-600 dark:text-gray-600 mb-4">
Choose how you'd like to begin working with your data
</p>
</div>
@@ -1932,11 +1932,11 @@ const TableEditor = () => {
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors group"
>
<Plus className="h-8 w-8 text-gray-400 group-hover:text-blue-500 dark:group-hover:text-blue-400 mb-2" />
<Plus className="h-8 w-8 text-gray-600 group-hover:text-blue-500 dark:group-hover:text-blue-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-blue-600 dark:group-hover:text-blue-400">
Start Empty
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
<span className="text-xs text-gray-600 dark:text-gray-600 text-center mt-1">
Create a blank table with basic columns
</span>
</button>
@@ -2005,11 +2005,11 @@ const TableEditor = () => {
}}
className="flex flex-col items-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-green-500 dark:hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors group"
>
<FileText className="h-8 w-8 text-gray-400 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
<FileText className="h-8 w-8 text-gray-600 group-hover:text-green-500 dark:group-hover:text-green-400 mb-2" />
<span className="font-medium text-gray-900 dark:text-gray-100 group-hover:text-green-600 dark:group-hover:text-green-400">
Load Sample
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center mt-1">
<span className="text-xs text-gray-600 dark:text-gray-600 text-center mt-1">
Start with example data to explore features
</span>
</button>
@@ -2055,7 +2055,7 @@ const TableEditor = () => {
{url && !isLoading && (
<button
onClick={() => setUrl("")}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-600 dark:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
<X className="h-4 w-4" />
</button>
@@ -2069,7 +2069,7 @@ const TableEditor = () => {
{isLoading ? "Fetching..." : "Fetch Data"}
</button>
</div>
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
<label className="flex items-center text-sm text-gray-600 dark:text-gray-600">
<input
type="checkbox"
checked={useFirstRowAsHeader}
@@ -2118,7 +2118,7 @@ const TableEditor = () => {
</div>
)}
<div className="flex items-center justify-between flex-shrink-0">
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
<label className="flex items-center text-sm text-gray-600 dark:text-gray-600">
<input
type="checkbox"
checked={useFirstRowAsHeader}
@@ -2161,7 +2161,7 @@ const TableEditor = () => {
onChange={handleFileUpload}
className="tool-input"
/>
<label className="flex items-center text-sm text-gray-600 dark:text-gray-400">
<label className="flex items-center text-sm text-gray-600 dark:text-gray-600">
<input
type="checkbox"
checked={useFirstRowAsHeader}
@@ -2204,7 +2204,7 @@ const TableEditor = () => {
{availableTables.length > 1 ? "Multi-Table Database" : "Table Editor"}
</h3>
{availableTables.length === 1 && (
<p className="text-sm text-gray-600 dark:text-gray-400">
<p className="text-sm text-gray-600 dark:text-gray-600">
{data.length} rows, {columns.length} columns
</p>
)}
@@ -2219,7 +2219,7 @@ const TableEditor = () => {
className={`flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors ${
isTableFullscreen
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
: "text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
}`}
>
{isTableFullscreen ? (
@@ -2233,7 +2233,7 @@ const TableEditor = () => {
</button>
<button
onClick={clearData}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
className="flex items-center gap-2 px-3 py-2 text-sm font-medium transition-colors border-l border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<AlertTriangle className="h-4 w-4" />
<span className="hidden sm:inline">Clear All</span>
@@ -2246,7 +2246,7 @@ const TableEditor = () => {
{availableTables.length > 1 && (
<div className="px-4 py-3 flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 border-b border-gray-200 dark:border-gray-700 justify-between">
<div className="flex items-center gap-2 w-full sm:max-w-1/2">
<span className="text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap hidden sm:inline">
<span className="text-sm text-gray-600 dark:text-gray-600 whitespace-nowrap hidden sm:inline">
Current Table:
</span>
<select
@@ -2267,7 +2267,7 @@ const TableEditor = () => {
})}
</select>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
<p className="text-sm text-gray-600 dark:text-gray-600">
{data.length} rows, {columns.length} columns
</p>
</div>
@@ -2279,7 +2279,7 @@ const TableEditor = () => {
<div className="px-4 py-3 flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 border-b border-gray-200 dark:border-gray-700">
{/* Search Bar */}
<div className="relative w-full">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-600" />
<input
type="text"
value={searchTerm}
@@ -2319,7 +2319,7 @@ const TableEditor = () => {
{/* Freeze Columns Control */}
<div className="flex items-center gap-2">
<span className="text-xs sm:text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap">
<span className="text-xs sm:text-sm text-gray-600 dark:text-gray-600 whitespace-nowrap">
Freeze:
</span>
<select
@@ -2354,7 +2354,7 @@ const TableEditor = () => {
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-[-1px] z-10">
<tr>
<th
className={`px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 tracking-wider border-r border-gray-200 dark:border-gray-600 ${
className={`px-4 py-3 text-left text-xs font-medium text-gray-600 dark:text-gray-300 tracking-wider border-r border-gray-200 dark:border-gray-600 ${
frozenColumns > 0
? "sticky left-0 z-20 bg-blue-50 dark:!bg-blue-900"
: ""
@@ -2390,7 +2390,7 @@ const TableEditor = () => {
return (
<th
key={column.id}
className={`relative px-4 py-3 text-left text-sm font-medium text-gray-500 dark:text-gray-300 tracking-wider hover:bg-gray-100 dark:hover:bg-gray-600 border-r border-gray-200 dark:border-gray-600 ${
className={`relative px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300 tracking-wider hover:bg-gray-100 dark:hover:bg-gray-600 border-r border-gray-200 dark:border-gray-600 ${
isFrozen
? "sticky z-20 bg-blue-50 dark:!bg-blue-900"
: ""
@@ -2450,7 +2450,7 @@ const TableEditor = () => {
className={`h-4 w-4 flex-shrink-0 ${
sortConfig.key === column.id
? "text-blue-600 dark:text-blue-400"
: "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
: "text-gray-600 hover:text-gray-600 dark:hover:text-gray-300"
}`}
/>
</button>
@@ -2471,7 +2471,7 @@ const TableEditor = () => {
<th className="px-4 py-3 text-center border-l-2 border-dashed border-gray-300 dark:border-gray-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 w-[60px]">
<button
onClick={addColumn}
className="flex items-center justify-center text-gray-500 hover:text-blue-600 p-2 rounded-lg transition-colors group"
className="flex items-center justify-center text-gray-600 hover:text-blue-600 p-2 rounded-lg transition-colors group"
title="Add new column"
>
<Plus className="h-4 w-4 group-hover:scale-110 transition-transform" />
@@ -2632,7 +2632,7 @@ const TableEditor = () => {
>
<span className="truncate block w-full">
{cellValue || (
<span className="text-gray-400 dark:text-gray-500 italic text-sm">
<span className="text-gray-600 dark:text-gray-600 italic text-sm">
Click to edit
</span>
)}
@@ -2662,7 +2662,7 @@ const TableEditor = () => {
>
<button
onClick={addRow}
className="flex items-center justify-center gap-2 text-gray-500 hover:text-blue-600 px-3 py-2 rounded-lg transition-colors group whitespace-nowrap sticky left-4"
className="flex items-center justify-center gap-2 text-gray-600 hover:text-blue-600 px-3 py-2 rounded-lg transition-colors group whitespace-nowrap sticky left-4"
title="Add new row"
>
<Plus className="h-4 w-4 group-hover:scale-110 transition-transform" />
@@ -2736,7 +2736,7 @@ const TableEditor = () => {
Export Results
{exportExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</h3>
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className="text-sm text-gray-600 dark:text-gray-600">
{availableTables.length > 1 ? (
<span>
Database: {originalFileName || "Multi-table"} (
@@ -2763,7 +2763,7 @@ const TableEditor = () => {
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "json"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
: "text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200"
}`}
>
<Braces className="h-4 w-4" />
@@ -2774,7 +2774,7 @@ const TableEditor = () => {
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "csv"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
: "text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200"
}`}
>
<FileText className="h-4 w-4" />
@@ -2785,7 +2785,7 @@ const TableEditor = () => {
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "tsv"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
: "text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200"
}`}
>
<FileText className="h-4 w-4" />
@@ -2796,7 +2796,7 @@ const TableEditor = () => {
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
exportTab === "sql"
? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
: "text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200"
}`}
>
<Database className="h-4 w-4" />
@@ -3318,7 +3318,7 @@ const ClearConfirmationModal = ({
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
This will permanently delete:
</h4>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<ul className="text-sm text-gray-600 dark:text-gray-600 space-y-1">
{tableCount > 1 ? (
<>
<li> {tableCount} tables</li>
@@ -3632,7 +3632,7 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
const renderVisualEditor = () => {
if (!isValid) {
return (
<div className="h-full flex items-center justify-center text-gray-500 dark:text-gray-400 p-6">
<div className="h-full flex items-center justify-center text-gray-600 dark:text-gray-600 p-6">
<div className="text-center">
<Code className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Invalid or unparseable data</p>
@@ -3666,7 +3666,7 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Object Editor
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
<p className="text-sm text-gray-600 dark:text-gray-600">
Row {modal.rowIndex} Column: {modal.columnName} Format:{" "}
{modal.format.type.replace("_", " ")}
</p>
@@ -3681,7 +3681,7 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
{isValid &&
structuredData &&
typeof structuredData === "object" && (
<span className="text-gray-600 dark:text-gray-400">
<span className="text-gray-600 dark:text-gray-600">
{" • "}{Object.keys(structuredData).length} properties
</span>
)}
@@ -3689,7 +3689,7 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
</div>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 self-start"
className="text-gray-600 hover:text-gray-600 dark:hover:text-gray-300 self-start"
>
<X className="h-6 w-6" />
</button>
@@ -3704,7 +3704,7 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
className={`px-3 py-2 text-sm font-medium rounded-md transition-colors ${
viewMode === "visual"
? "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
: "text-gray-600 hover:text-gray-900 dark:text-gray-600 dark:hover:text-gray-200"
}`}
>
<Edit3 className="h-4 w-4 inline mr-2" />
@@ -3715,7 +3715,7 @@ const ObjectEditorModal = ({ modal, onClose, onApply }) => {
className={`px-3 py-2 text-sm font-medium rounded-md transition-colors ${
viewMode === "raw"
? "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
: "text-gray-600 hover:text-gray-900 dark:text-gray-600 dark:hover:text-gray-200"
}`}
>
<Code className="h-4 w-4 inline mr-2" />
@@ -3845,7 +3845,7 @@ const InputChangeConfirmationModal = ({
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
This will permanently delete:
</h4>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<ul className="text-sm text-gray-600 dark:text-gray-600 space-y-1">
{tableCount > 1 ? (
<>
<li> {tableCount} imported tables</li>

0
src/pages/TermsOfService.js Normal file → Executable file
View File

20
src/pages/TextLengthTool.js Normal file → Executable file
View File

@@ -172,7 +172,7 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
{url && !isLoading && (
<button
onClick={clearUrl}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-600 dark:text-gray-600 dark:hover:text-gray-300 transition-colors"
>
<X className="h-4 w-4" />
</button>
@@ -208,7 +208,7 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
{CONTENT_TYPE_INFO[urlResult.contentType].label}
</span>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
<div className="text-sm text-gray-600 dark:text-gray-600 mb-2">
{CONTENT_TYPE_INFO[urlResult.contentType].description}
</div>
{urlResult.title && (
@@ -216,7 +216,7 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
{urlResult.title}
</div>
)}
<div className="text-xs text-gray-500 dark:text-gray-400">
<div className="text-xs text-gray-600 dark:text-gray-600">
Article: {urlResult.metrics.articleWordCount} words
Total: {urlResult.metrics.totalWordCount} words
Ratio: {Math.round(urlResult.metrics.contentRatio * 100)}%
@@ -336,28 +336,28 @@ Typing time: ${getTypingTime()}` : ''}`;
<div className="text-2xl font-bold text-primary-600 dark:text-primary-400">
{formatNumber(stats.characters)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Characters</div>
<div className="text-sm text-gray-600 dark:text-gray-600">Characters</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-2xl font-bold text-primary-600 dark:text-primary-400">
{formatNumber(stats.charactersNoSpaces)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Characters (no spaces)</div>
<div className="text-sm text-gray-600 dark:text-gray-600">Characters (no spaces)</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
{formatNumber(stats.words)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Words</div>
<div className="text-sm text-gray-600 dark:text-gray-600">Words</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
{formatNumber(stats.lines)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Lines</div>
<div className="text-sm text-gray-600 dark:text-gray-600">Lines</div>
</div>
</div>
@@ -369,14 +369,14 @@ Typing time: ${getTypingTime()}` : ''}`;
<div className="text-xl font-bold text-purple-600 dark:text-purple-400">
{formatNumber(stats.sentences)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Sentences</div>
<div className="text-sm text-gray-600 dark:text-gray-600">Sentences</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-xl font-bold text-orange-600 dark:text-orange-400">
{formatNumber(stats.paragraphs)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Paragraphs</div>
<div className="text-sm text-gray-600 dark:text-gray-600">Paragraphs</div>
</div>
</div>
@@ -384,7 +384,7 @@ Typing time: ${getTypingTime()}` : ''}`;
<div className="text-xl font-bold text-red-600 dark:text-red-400">
{formatNumber(stats.bytes)} bytes
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Size (UTF-8 encoding)</div>
<div className="text-sm text-gray-600 dark:text-gray-600">Size (UTF-8 encoding)</div>
</div>
{/* Reading & Typing Time */}

24
src/pages/UrlTool.js Normal file → Executable file
View File

@@ -60,7 +60,7 @@ const UrlTool = () => {
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'encode'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
: 'text-gray-600 dark:text-gray-600'
}`}
>
Encode
@@ -70,7 +70,7 @@ const UrlTool = () => {
className={`px-4 py-2 rounded-md font-medium transition-colors ${
mode === 'decode'
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
: 'text-gray-600 dark:text-gray-400'
: 'text-gray-600 dark:text-gray-600'
}`}
>
Decode
@@ -112,7 +112,7 @@ const UrlTool = () => {
</div>
{/* Output */}
<div className="space-y-2">
<div className="space-y-2" aria-live="polite">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'encode' ? 'Encoded URL' : 'Decoded URL'}
</label>
@@ -125,7 +125,7 @@ const UrlTool = () => {
? 'Encoded URL will appear here...'
: 'Decoded URL will appear here...'
}
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
className={`tool-input h-96 bg-gray-50 dark:bg-gray-800 ${output?.startsWith('Error:') ? 'border-red-300 dark:border-red-700' : ''}`}
/>
{output && <CopyButton text={output} />}
</div>
@@ -137,35 +137,35 @@ const UrlTool = () => {
<h4 className="text-gray-800 dark:text-gray-200 font-medium mb-3">Common URL Encoding Reference</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-gray-600 dark:text-gray-400">Space:</span>
<span className="text-gray-600 dark:text-gray-600">Space:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">%20</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">!:</span>
<span className="text-gray-600 dark:text-gray-600">!:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">%21</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">#:</span>
<span className="text-gray-600 dark:text-gray-600">#:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">%23</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">$:</span>
<span className="text-gray-600 dark:text-gray-600">$:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">%24</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">&:</span>
<span className="text-gray-600 dark:text-gray-600">&:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">%26</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">':</span>
<span className="text-gray-600 dark:text-gray-600">':</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">%27</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">(:</span>
<span className="text-gray-600 dark:text-gray-600">(:</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">%28</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">):</span>
<span className="text-gray-600 dark:text-gray-600">):</span>
<span className="ml-2 font-mono text-gray-800 dark:text-gray-200">%29</span>
</div>
</div>

8
src/pages/components/CodeInputsNew.js Normal file → Executable file
View File

@@ -96,7 +96,7 @@ const CodeInputs = ({
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
: 'border-transparent text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
{tab.label}
@@ -108,7 +108,7 @@ const CodeInputs = ({
<div className="flex items-center justify-end space-x-2 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => handleSearch(getCurrentEditorRef())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Search"
>
<Search className="w-3 h-3" />
@@ -116,7 +116,7 @@ const CodeInputs = ({
</button>
<button
onClick={() => handleCopy(getCurrentContent())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Copy"
>
<Copy className="w-3 h-3" />
@@ -124,7 +124,7 @@ const CodeInputs = ({
</button>
<button
onClick={() => handleExport(getCurrentContent(), getExportFilename())}
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
title="Export"
>
<Download className="w-3 h-3" />

6
src/pages/components/ElementEditor.js Normal file → Executable file
View File

@@ -154,14 +154,14 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
<div className="flex items-center space-x-2">
<button
onClick={handleCopyElement}
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
className="p-1 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-200 transition-colors"
title="Copy element"
>
<Copy className="w-4 h-4" />
</button>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
className="text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-200"
>
</button>
@@ -211,7 +211,7 @@ const ElementEditor = ({ htmlInput, setHtmlInput, onClose, onSave, previewFrameR
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Inner HTML
<span className="text-xs text-gray-500 ml-2">(HTML content inside element)</span>
<span className="text-xs text-gray-600 ml-2">(HTML content inside element)</span>
</label>
<textarea
ref={(el) => textareaRefs.current.innerHTML = el}

0
src/pages/components/PreviewFrame.js Normal file → Executable file
View File

0
src/pages/components/PreviewServer.js Normal file → Executable file
View File

10
src/pages/components/SimpleToolbar.js Normal file → Executable file
View File

@@ -38,8 +38,8 @@ const SimpleToolbar = ({
selectedDevice === device.id
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
: isDisabled
? 'text-gray-400 dark:text-gray-600 cursor-not-allowed'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
? 'text-gray-600 dark:text-gray-600 cursor-not-allowed'
: 'text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
title={isDisabled ? 'Disabled when sidebar is expanded' : `Switch to ${device.label} view`}
>
@@ -55,7 +55,7 @@ const SimpleToolbar = ({
{/* Refresh button */}
<button
onClick={onRefresh}
className="flex items-center gap-2 px-3 py-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
className="flex items-center gap-2 px-3 py-2 text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
title="Refresh preview"
>
<RotateCcw className="w-4 h-4" />
@@ -65,7 +65,7 @@ const SimpleToolbar = ({
{/* Sidebar toggle */}
<button
onClick={onToggleSidebar}
className="flex items-center gap-2 px-3 py-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
className="flex items-center gap-2 px-3 py-2 text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
title={showSidebar ? 'Hide sidebar' : 'Show sidebar'}
>
{showSidebar ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
@@ -75,7 +75,7 @@ const SimpleToolbar = ({
{/* Fullscreen toggle */}
<button
onClick={onToggleFullscreen}
className="flex items-center gap-2 px-3 py-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
className="flex items-center gap-2 px-3 py-2 text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
>
{isFullscreen ? <Minimize className="w-4 h-4" /> : <Maximize className="w-4 h-4" />}

14
src/pages/components/Toolbar.js Normal file → Executable file
View File

@@ -43,7 +43,7 @@ const Toolbar = ({
className={`p-2 rounded-md transition-colors ${
showSidebar
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
title="Toggle Code Sidebar"
>
@@ -56,7 +56,7 @@ const Toolbar = ({
<div className="flex items-center space-x-2">
<button
onClick={onRefresh}
className="p-2 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="p-2 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
title="Refresh Preview"
>
<RefreshCw className="w-4 h-4" />
@@ -68,7 +68,7 @@ const Toolbar = ({
className={`p-2 rounded transition-colors ${
selectedDevice === 'desktop'
? 'bg-blue-500 text-white'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
: 'text-gray-600 dark:text-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
title="Desktop View"
>
@@ -79,7 +79,7 @@ const Toolbar = ({
className={`p-2 rounded transition-colors ${
selectedDevice === 'tablet'
? 'bg-blue-500 text-white'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
: 'text-gray-600 dark:text-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
title="Tablet View"
>
@@ -90,7 +90,7 @@ const Toolbar = ({
className={`p-2 rounded transition-colors ${
selectedDevice === 'mobile'
? 'bg-blue-500 text-white'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
: 'text-gray-600 dark:text-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
title="Mobile View"
>
@@ -104,7 +104,7 @@ const Toolbar = ({
className={`p-2 rounded-md transition-colors ${
isInspectModeActive
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
title="Inspect Element"
>
@@ -117,7 +117,7 @@ const Toolbar = ({
<div className="flex items-center space-x-2">
<button
onClick={() => onToggleFullscreen()}
className="p-2 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
className="p-2 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-600 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
title={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
>
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}

0
src/styles/device-frames.css Normal file → Executable file
View File

0
src/styles/diff-theme.css Normal file → Executable file
View File

420
src/styles/markdown-preview.css Normal file → Executable file
View File

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

Some files were not shown because too many files have changed in this diff Show More