chore: remove OfferBlock learn more button and change to coming soon
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
44871
package-lock.json
generated
Normal file → Executable file
3
package.json
Normal file → Executable file
@@ -89,8 +89,7 @@
|
|||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"react-app"
|
||||||
"react-app/jest"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"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 |
0
public/data/commits.json
Normal file → Executable file
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
10
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', adDomain = 'solutionbiologyisle.com' }) => {
|
const AdBlock = ({
|
||||||
|
className = "",
|
||||||
|
adKey = "e0ca7c61c83457f093bbc2e261b43d31",
|
||||||
|
adDomain = "www.highperformanceformat.com",
|
||||||
|
}) => {
|
||||||
const iframeRef = useRef(null);
|
const iframeRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,7 +43,7 @@ const AdBlock = ({ className = '', adKey = 'e0ca7c61c83457f093bbc2e261b43d31', a
|
|||||||
<iframe
|
<iframe
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
className={`bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${className}`}
|
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"
|
title="Advertisement"
|
||||||
sandbox="allow-scripts allow-same-origin"
|
sandbox="allow-scripts allow-same-origin"
|
||||||
/>
|
/>
|
||||||
|
|||||||
0
src/components/AdColumn.js
Normal file → Executable file
0
src/components/AdvancedURLFetch.js
Normal file → Executable file
0
src/components/AffiliateBlock.js
Normal file → Executable file
0
src/components/CodeEditor.js
Normal file → Executable file
0
src/components/CodeMirrorEditor.js
Normal file → Executable file
0
src/components/ConsentBanner.js
Normal file → Executable file
0
src/components/CopyButton.js
Normal file → Executable file
0
src/components/ErrorBoundary.js
Normal file → Executable file
0
src/components/Layout.js
Normal file → Executable file
0
src/components/Loading.js
Normal file → Executable file
0
src/components/MindmapView.js
Normal file → Executable file
14
src/components/MobileAdBanner.js
Normal file → Executable file
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { X } from 'lucide-react';
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
const MobileAdBanner = () => {
|
const MobileAdBanner = () => {
|
||||||
const [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(true);
|
||||||
@@ -7,8 +7,8 @@ const MobileAdBanner = () => {
|
|||||||
const iframeRef = useRef(null);
|
const iframeRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const wasClosed = sessionStorage.getItem('mobileAdClosed');
|
const wasClosed = sessionStorage.getItem("mobileAdClosed");
|
||||||
if (wasClosed === 'true') {
|
if (wasClosed === "true") {
|
||||||
setClosed(true);
|
setClosed(true);
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ const MobileAdBanner = () => {
|
|||||||
'params' : {}
|
'params' : {}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript" src="https://solutionbiologyisle.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
|
<script type="text/javascript" src="https://www.highperformanceformat.com/2965bcf877388cafa84160592c550f5a/invoke.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`);
|
||||||
@@ -55,7 +55,7 @@ const MobileAdBanner = () => {
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
setClosed(true);
|
setClosed(true);
|
||||||
sessionStorage.setItem('mobileAdClosed', 'true');
|
sessionStorage.setItem("mobileAdClosed", "true");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!visible || closed) return null;
|
if (!visible || closed) return null;
|
||||||
@@ -72,7 +72,7 @@ const MobileAdBanner = () => {
|
|||||||
<div className="flex justify-center items-center py-2">
|
<div className="flex justify-center items-center py-2">
|
||||||
<iframe
|
<iframe
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
style={{ width: '320px', height: '50px', border: 'none' }}
|
style={{ width: "320px", height: "50px", border: "none" }}
|
||||||
title="Mobile Advertisement"
|
title="Mobile Advertisement"
|
||||||
sandbox="allow-scripts allow-same-origin"
|
sandbox="allow-scripts allow-same-origin"
|
||||||
/>
|
/>
|
||||||
|
|||||||
0
src/components/NavigationConfirmModal.js
Normal file → Executable file
18
src/components/OfferBlock.js
Normal file → Executable file
@@ -1,20 +1,16 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
const OfferBlock = () => {
|
const OfferBlock = () => {
|
||||||
return (
|
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">
|
<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">
|
<span className="bg-white/20 text-xs font-bold px-2 py-1 rounded mb-4 backdrop-blur-sm uppercase tracking-wider">
|
||||||
SPECIAL OFFER
|
Coming Soon
|
||||||
</span>
|
</span>
|
||||||
<h3 className="text-2xl font-bold mb-2">
|
<h3 className="text-2xl font-bold mb-2">Upgrade to PRO</h3>
|
||||||
Upgrade to PRO
|
<p className="text-indigo-100 text-sm mb-6 leading-relaxed">
|
||||||
</h3>
|
We are preparing a premium ad-free experience with exclusive developer
|
||||||
<p className="text-indigo-100 text-sm mb-6">
|
tools and features. Stay tuned!
|
||||||
Get unlimited access to all developer tools and features.
|
|
||||||
</p>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
0
src/components/PostmanTable.js
Normal file → Executable file
0
src/components/PostmanTreeTable.js
Normal file → Executable file
0
src/components/ProBadge.js
Normal file → Executable file
0
src/components/RelatedTools.js
Normal file → Executable file
0
src/components/SEO.js
Normal file → Executable file
0
src/components/SEOHead.js
Normal file → Executable file
0
src/components/StructuredEditor.js
Normal file → Executable file
13
src/components/TabletAdSection.js
Normal file → Executable file
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import AdBlock from './AdBlock';
|
import AdBlock from "./AdBlock";
|
||||||
import OfferBlock from './OfferBlock';
|
import OfferBlock from "./OfferBlock";
|
||||||
import AffiliateBlock from './AffiliateBlock';
|
import AffiliateBlock from "./AffiliateBlock";
|
||||||
|
|
||||||
const TabletAdSection = () => {
|
const TabletAdSection = () => {
|
||||||
return (
|
return (
|
||||||
@@ -11,7 +11,10 @@ const TabletAdSection = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="flex justify-center gap-4 overflow-x-auto pb-4">
|
<div className="flex justify-center gap-4 overflow-x-auto pb-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<AdBlock adKey="7c55aebcdd74f6e9a8dc24bd13e7d949" adDomain="solutionbiologyisle.com" />
|
<AdBlock
|
||||||
|
adKey="7c55aebcdd74f6e9a8dc24bd13e7d949"
|
||||||
|
adDomain="www.highperformanceformat.com"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<OfferBlock />
|
<OfferBlock />
|
||||||
|
|||||||