Compare commits
2 Commits
9dc3285adb
...
6a14eebf25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a14eebf25 | ||
|
|
3a475e9df2 |
BIN
._node_modules
Executable file
BIN
._package-lock.json
generated
Executable file
BIN
._package.json
Executable file
0
.env.example
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable 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
0
DOCUMENTATION_INDEX.md
Normal file → Executable file
0
EDITOR_CHECKLIST.md
Normal file → Executable file
0
EDITOR_TOOL_GUIDE.md
Normal file → Executable file
0
FEATURE_TOGGLE_GUIDE.md
Normal file → Executable file
811
PLAN_ACCOUNTS_AND_STORAGE.md
Executable 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
0
SEO_IMPROVEMENT_PLAN.md
Normal file → Executable file
0
nixpacks.toml
Normal file → Executable file
44869
package-lock.json
generated
Normal file → Executable file
3
package.json
Normal file → Executable file
@@ -89,8 +89,7 @@
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
0
postcss.config.js
Normal file → Executable file
0
public/ads.txt
Normal file → Executable file
0
public/android-chrome-192x192.png
Normal file → Executable file
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
0
public/android-chrome-512x512.png
Normal file → Executable file
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
0
public/apple-touch-icon.png
Normal file → Executable file
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
41
public/data/commits.json
Normal file → Executable 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
0
public/favicon-16x16.png
Normal file → Executable file
|
Before Width: | Height: | Size: 962 B After Width: | Height: | Size: 962 B |
0
public/favicon-32x32.png
Normal file → Executable file
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
0
public/favicon.ico
Normal file → Executable file
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
0
public/icon-192x192.png
Normal file → Executable file
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
0
public/icon-512x512.png
Normal file → Executable file
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
0
public/images/onidel-banner.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
0
public/index.html
Normal file → Executable file
0
public/logo.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
0
public/manifest.json
Normal file → Executable file
0
public/robots.txt
Normal file → Executable file
0
public/sitemap.xml
Normal file → Executable file
0
src/App.js
Normal file → Executable file
BIN
src/components/._AdBlock.js
Executable file
BIN
src/components/._MobileAdBanner.js
Executable file
BIN
src/components/._OfferBlock.js
Executable file
BIN
src/components/._TabletAdSection.js
Executable file
14
src/components/AdBlock.js
Normal file → Executable 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
6
src/components/AdvancedURLFetch.js
Normal file → Executable 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
0
src/components/CodeEditor.js
Normal file → Executable file
2
src/components/CodeMirrorEditor.js
Normal file → Executable 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
0
src/components/SEOHead.js
Normal file → Executable file
18
src/components/StructuredEditor.js
Normal file → Executable 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>
|
||||
|
||||
30
src/components/TabletAdSection.js
Executable 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
6
src/components/ToolCard.js
Normal file → Executable 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
@@ -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
@@ -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
0
src/config/features.js
Normal file → Executable file
2
src/config/tools.js
Normal file → Executable 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
0
src/hooks/useAnalytics.js
Normal file → Executable file
0
src/hooks/useNavigationGuard.js
Normal file → Executable file
0
src/index.css
Normal file → Executable file
0
src/index.js
Normal file → Executable file
8
src/pages/Base64Tool.js
Normal file → Executable 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
20
src/pages/TextLengthTool.js
Normal file → Executable 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
@@ -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
@@ -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
@@ -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
0
src/pages/components/PreviewServer.js
Normal file → Executable file
10
src/pages/components/SimpleToolbar.js
Normal file → Executable 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
@@ -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" />}
|
||||
|
||||