Compare commits

...

2 Commits

Author SHA1 Message Date
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
112 changed files with 35302 additions and 11144 deletions

BIN
._node_modules Executable file

Binary file not shown.

BIN
._package-lock.json generated Executable file

Binary file not shown.

BIN
._package.json Executable file

Binary file not shown.

0
.env Normal file → Executable file
View File

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

0
.gitignore vendored Normal file → Executable file
View File

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
```

0
PROJECT_ROADMAP.md Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

0
SEO_IMPROVEMENT_PLAN.md Normal file → Executable file
View File

0
TODO.md Normal file → Executable file
View File

0
nixpacks.toml Normal file → Executable file
View File

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

File diff suppressed because it is too large Load Diff

3
package.json Normal file → Executable file
View File

@@ -89,8 +89,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

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

@@ -1,5 +1,46 @@
{
"changelog": [
{
"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

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

BIN
src/components/._AdBlock.js Executable file

Binary file not shown.

Binary file not shown.

BIN
src/components/._OfferBlock.js Executable file

Binary file not shown.

Binary file not shown.

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 = "www.highperformanceformat.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

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>

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

@@ -108,6 +108,8 @@ 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>
@@ -143,10 +145,10 @@ const Layout = ({ children }) => {
</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,6 +166,8 @@ 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" />}
</button>
@@ -211,7 +215,7 @@ const Layout = ({ children }) => {
})}
<div className="border-t border-slate-200/50 dark:border-slate-700/50 pt-4 mt-4">
<div className="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider px-4 py-2 flex items-center gap-2">
<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>
@@ -237,7 +241,7 @@ const Layout = ({ children }) => {
</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>
</button>
);
@@ -305,16 +309,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>
@@ -331,21 +335,21 @@ const Layout = ({ children }) => {
<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"
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"
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"
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</button>
@@ -379,7 +383,7 @@ const Layout = ({ children }) => {
</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>
@@ -387,21 +391,21 @@ const Layout = ({ children }) => {
<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"
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"
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"
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>

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

@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { X } from 'lucide-react';
import React, { useEffect, useRef, useState } from "react";
import { X } from "lucide-react";
const MobileAdBanner = () => {
const [visible, setVisible] = useState(true);
@@ -7,8 +7,8 @@ const MobileAdBanner = () => {
const iframeRef = useRef(null);
useEffect(() => {
const wasClosed = sessionStorage.getItem('mobileAdClosed');
if (wasClosed === 'true') {
const wasClosed = sessionStorage.getItem("mobileAdClosed");
if (wasClosed === "true") {
setClosed(true);
setVisible(false);
}
@@ -42,7 +42,7 @@ const MobileAdBanner = () => {
'params' : {}
};
</script>
<script type="text/javascript" src="https://bustleplaguereed.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
<script type="text/javascript" src="https://www.highperformanceformat.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
</body>
</html>
`);
@@ -55,16 +55,16 @@ const MobileAdBanner = () => {
const handleClose = () => {
setVisible(false);
setClosed(true);
sessionStorage.setItem('mobileAdClosed', '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">
<div className="lg:hidden fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 shadow-lg border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleClose}
className="absolute -top-2 right-2 p-1 text-gray-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"
className="absolute -top-2 right-2 p-1 text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-200 bg-white dark:bg-gray-800 rounded-full shadow-sm z-10"
aria-label="Close ad"
>
<X className="h-4 w-4" />
@@ -72,7 +72,7 @@ const MobileAdBanner = () => {
<div className="flex justify-center items-center py-2">
<iframe
ref={iframeRef}
style={{ width: '320px', height: '50px', border: 'none' }}
style={{ width: "320px", height: "50px", border: "none" }}
title="Mobile Advertisement"
sandbox="allow-scripts allow-same-origin"
/>

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

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

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

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

@@ -555,7 +555,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
// Array items: icon + index span (compact)
<>
{getTypeIcon(value)}
<span className="px-2 py-1 text-sm text-gray-600 dark:text-gray-400 font-mono whitespace-nowrap">
<span className="px-2 py-1 text-sm text-gray-600 dark:text-gray-600 font-mono whitespace-nowrap">
[{key}]
</span>
</>
@@ -587,7 +587,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
style={{width: '120px'}} // Fixed width for consistency
/>
)}
<span className="text-gray-500 inline">:</span>
<span className="text-gray-600 inline">:</span>
</>
)}
@@ -612,7 +612,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
}`}
/>
</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>
</>
@@ -667,7 +667,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
</div>
)
) : (
<span className="flex-1 text-sm text-gray-600 dark:text-gray-400">
<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>
)}
@@ -762,7 +762,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
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'
: '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" />
@@ -773,7 +773,7 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
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'
: '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" />
@@ -788,7 +788,7 @@ 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>
</div>
@@ -822,13 +822,13 @@ const StructuredEditor = ({ onDataChange, initialData = {}, readOnly: readOnlyPr
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Edit Nested {nestedEditModal.type === 'json' ? 'JSON' : 'Serialized'} Data
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
<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="www.highperformanceformat.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

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

@@ -7,7 +7,7 @@ export const TOOL_CATEGORIES = {
color: 'from-slate-500 to-slate-600',
hoverColor: 'slate-600',
textColor: 'text-slate-600',
hoverTextColor: 'hover:text-slate-700 dark:hover:text-slate-400'
hoverTextColor: 'hover:text-slate-700 dark:hover:text-slate-600'
},
editor: {
name: 'Editor',

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

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

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>

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>

64
src/pages/MarkdownEditor.js Normal file → Executable file
View File

@@ -1517,7 +1517,7 @@ ${html}
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" />
@@ -1528,7 +1528,7 @@ ${html}
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" />
@@ -1539,7 +1539,7 @@ ${html}
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" />
@@ -1550,7 +1550,7 @@ ${html}
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" />
@@ -1571,7 +1571,7 @@ ${html}
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Create New Markdown Document
</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 writing
</p>
</div>
@@ -1590,11 +1590,11 @@ ${html}
}}
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">
Begin with a blank markdown document
</span>
</button>
@@ -1628,11 +1628,11 @@ ${html}
}}
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 Template
</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 a pre-made template
</span>
</button>
@@ -1704,7 +1704,7 @@ ${html}
{fetching ? '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 a URL to a markdown file (GitHub raw, Gist, Pastebin, etc.)
</p>
</div>
@@ -1749,7 +1749,7 @@ ${html}
</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">
Paste markdown text
</div>
<button
@@ -1826,7 +1826,7 @@ ${html}
</h3>
{/* Statistics */}
<div className="hidden sm:flex items-center gap-3 text-xs text-gray-600 dark:text-gray-400">
<div className="hidden sm:flex items-center gap-3 text-xs text-gray-600 dark:text-gray-600">
<span>{stats.words} words</span>
<span></span>
<span>{stats.characters} chars</span>
@@ -1845,7 +1845,7 @@ ${html}
className={`flex items-center gap-2 px-3 py-2 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 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'
}`}
title="Editor Only"
>
@@ -1858,7 +1858,7 @@ ${html}
className={`hidden lg: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 === 'split'
? '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'
}`}
title="Split View"
>
@@ -1870,7 +1870,7 @@ ${html}
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 === 'preview'
? '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'
}`}
title="Preview Only"
>
@@ -1881,7 +1881,7 @@ ${html}
<button
onClick={() => setIsFullscreen(!isFullscreen)}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
className="p-2 text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 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" />}
@@ -1909,7 +1909,7 @@ ${html}
<div className="relative group">
<button
onClick={btn.action}
className="p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
className="p-2 text-gray-600 dark:text-gray-600 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
aria-label={btn.label}
>
<Icon className="h-4 w-4" />
@@ -1977,7 +1977,7 @@ You can use:
dangerouslySetInnerHTML={{ __html: parseMarkdown(markdownText) }}
/>
) : (
<div className="flex items-center justify-center h-full text-gray-400 dark:text-gray-500">
<div className="flex items-center justify-center h-full text-gray-600 dark:text-gray-600">
<div className="text-center">
<EyeOff className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p className="text-sm">Preview will appear here</p>
@@ -1999,7 +1999,7 @@ You can use:
<Download className="h-5 w-5 text-blue-600 dark:text-blue-400" />
Export Options
</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">
Download your markdown in different formats
</p>
</div>
@@ -2011,9 +2011,9 @@ You can use:
onClick={handleExportMarkdown}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all group relative"
>
<FileText className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 mb-3" />
<FileText className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-blue-600 dark:group-hover:text-blue-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">Markdown</span>
<span className="text-xs text-gray-500 dark:text-gray-400">.md file</span>
<span className="text-xs text-gray-600 dark:text-gray-600">.md file</span>
</button>
{/* Export as PDF */}
@@ -2021,9 +2021,9 @@ You can use:
onClick={handleExportPDF}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-red-500 dark:hover:border-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all group relative"
>
<FileDown className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-400 group-hover:text-red-600 dark:group-hover:text-red-400 mb-3" />
<FileDown className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-red-600 dark:group-hover:text-red-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">PDF</span>
<span className="text-xs text-gray-500 dark:text-gray-400">.pdf file</span>
<span className="text-xs text-gray-600 dark:text-gray-600">.pdf file</span>
</button>
{/* Export as Full HTML */}
@@ -2031,9 +2031,9 @@ You can use:
onClick={handleExportHTML}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-green-500 dark:hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20 transition-all group relative"
>
<Globe className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-400 group-hover:text-green-600 dark:group-hover:text-green-400 mb-3" />
<Globe className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-green-600 dark:group-hover:text-green-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">Full HTML</span>
<span className="text-xs text-gray-500 dark:text-gray-400">.html page</span>
<span className="text-xs text-gray-600 dark:text-gray-600">.html page</span>
</button>
{/* Export as HTML Content Only */}
@@ -2041,9 +2041,9 @@ You can use:
onClick={handleExportHTMLContent}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-teal-500 dark:hover:border-teal-400 hover:bg-teal-50 dark:hover:bg-teal-900/20 transition-all group relative"
>
<Code className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-400 group-hover:text-teal-600 dark:group-hover:text-teal-400 mb-3" />
<Code className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-teal-600 dark:group-hover:text-teal-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">HTML Content</span>
<span className="text-xs text-gray-500 dark:text-gray-400">Body only</span>
<span className="text-xs text-gray-600 dark:text-gray-600">Body only</span>
</button>
{/* Export as Plain Text */}
@@ -2051,9 +2051,9 @@ You can use:
onClick={handleExportPlainText}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 dark:hover:border-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-all group relative"
>
<Type className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-400 group-hover:text-purple-600 dark:group-hover:text-purple-400 mb-3" />
<Type className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-purple-600 dark:group-hover:text-purple-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">Plain Text</span>
<span className="text-xs text-gray-500 dark:text-gray-400">.txt file</span>
<span className="text-xs text-gray-600 dark:text-gray-600">.txt file</span>
</button>
{/* Copy to Clipboard */}
@@ -2061,9 +2061,9 @@ You can use:
onClick={handleCopyToClipboard}
className="flex flex-col items-center justify-center p-4 md:p-6 border-2 border-gray-200 dark:border-gray-700 rounded-lg hover:border-orange-500 dark:hover:border-orange-400 hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-all group relative"
>
<Download className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-400 group-hover:text-orange-600 dark:group-hover:text-orange-400 mb-3" />
<Download className="h-8 w-8 absolute left-4 top-5 md:static md:left-0 md:top-0 text-gray-600 dark:text-gray-600 group-hover:text-orange-600 dark:group-hover:text-orange-400 mb-3" />
<span className="font-medium text-gray-900 dark:text-white mb-1">Copy</span>
<span className="text-xs text-gray-500 dark:text-gray-400">To clipboard</span>
<span className="text-xs text-gray-600 dark:text-gray-600">To clipboard</span>
</button>
</div>
@@ -2224,7 +2224,7 @@ const InputChangeConfirmationModal = ({ markdownText, stats, currentMethod, newM
<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> {stats.words} words of markdown content</li>
<li> {stats.characters} characters</li>
<li> {stats.lines} lines</li>

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" />}

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