814 lines
43 KiB
HTML
814 lines
43 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru" class="dark">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>HTTP Client — images.wadevelop.ru</title>
|
|
<script src="/vendor/tailwind.js"></script>
|
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="/vendor/hljs-github-dark.css">
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
mono: ['JetBrains Mono', 'monospace'],
|
|
sans: ['Manrope', 'sans-serif'],
|
|
},
|
|
colors: {
|
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
|
warn: '#ffd600',
|
|
danger: '#ff5252',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
|
.main-content{margin-left:56px}
|
|
@media(max-width:640px){
|
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
|
.sidebar-logo{display:none}
|
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
|
.main-content{margin-left:0;padding-bottom:60px}
|
|
}
|
|
:root{--bg:#f5f5f7;--bg-grad1:rgba(0,67,184,.04);--bg-grad2:rgba(0,50,140,.03);--surface-800:rgba(255,255,255,0.85);--surface-700:#f0f0f3;--surface-600:#dde0e6;--text-primary:#1a1a2e;--text-secondary:#555;--text-muted:#888;--select-bg:#fff;--select-color:#1a1a2e}
|
|
.dark{--bg:#060c18;--bg-grad1:rgba(0,84,230,.03);--bg-grad2:rgba(0,67,184,.02);--surface-800:rgba(9,16,32,0.8);--surface-700:#0d1828;--surface-600:#162040;--text-primary:#fff;--text-secondary:#c0c0c0;--text-muted:#666;--select-bg:#091020;--select-color:#e0e0e0}
|
|
body{background:var(--bg);background-image:radial-gradient(ellipse at 20% 50%,var(--bg-grad1) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,var(--bg-grad2) 0%,transparent 50%);min-height:100vh;transition:background .3s}
|
|
.dark .noise::before{content:'';position:fixed;inset:0;background:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");pointer-events:none;z-index:0}
|
|
.theme-toggle{position:relative;width:44px;height:24px;border-radius:12px;cursor:pointer;transition:background .3s}
|
|
.dark .theme-toggle{background:#162040}
|
|
.theme-toggle{background:#dde0e6}
|
|
.theme-toggle-knob{position:absolute;top:2px;width:20px;height:20px;border-radius:50%;transition:left .3s,background .3s;display:flex;align-items:center;justify-content:center}
|
|
.dark .theme-toggle-knob{left:22px;background:#0d1828}
|
|
.theme-toggle-knob{left:2px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.15)}
|
|
|
|
/* HTTP Client UI */
|
|
.panel {
|
|
background: var(--surface-800);
|
|
border: 1px solid var(--surface-600);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
.tab-btn {
|
|
padding: 6px 14px;
|
|
font-size: 12px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: var(--text-muted);
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
transition: all .2s;
|
|
white-space: nowrap;
|
|
}
|
|
.tab-btn:hover { color: var(--text-secondary); background: rgba(0,84,230,.05); }
|
|
.tab-btn.active { color: #0054e6; background: rgba(0,84,230,.08); }
|
|
.kv-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
|
|
.kv-row input {
|
|
flex: 1;
|
|
background: var(--surface-800);
|
|
border: 1px solid var(--surface-600);
|
|
border-radius: 6px;
|
|
padding: 5px 8px;
|
|
font-size: 12px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: var(--select-color);
|
|
outline: none;
|
|
min-width: 0;
|
|
}
|
|
.kv-row input:focus { border-color: rgba(0,84,230,.5); }
|
|
.kv-row input::placeholder { color: var(--text-muted); }
|
|
.kv-del {
|
|
width: 24px; height: 24px;
|
|
border-radius: 6px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
cursor: pointer;
|
|
color: var(--text-muted);
|
|
transition: all .15s;
|
|
flex-shrink: 0;
|
|
}
|
|
.kv-del:hover { color: #ff5252; background: rgba(255,82,82,.1); }
|
|
.kv-add {
|
|
font-size: 11px; font-family: 'JetBrains Mono', monospace;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
padding: 4px 0;
|
|
transition: color .15s;
|
|
}
|
|
.kv-add:hover { color: #0054e6; }
|
|
.method-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 1px 7px;
|
|
border-radius: 4px;
|
|
font-size: 10px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-weight: 700;
|
|
}
|
|
.m-GET { background: rgba(0,168,107,.15); color: #00a86b; }
|
|
.m-POST { background: rgba(255,165,0,.15); color: #ffa500; }
|
|
.m-PUT { background: rgba(0,84,230,.15); color: #6b9fff; }
|
|
.m-PATCH { background: rgba(148,0,211,.15); color: #cc66ff; }
|
|
.m-DELETE { background: rgba(255,82,82,.15); color: #ff5252; }
|
|
.m-HEAD { background: rgba(150,150,150,.15); color: #aaa; }
|
|
.m-OPTIONS{ background: rgba(150,150,150,.15); color: #aaa; }
|
|
.status-2xx { color: #00a86b; }
|
|
.status-3xx { color: #6b9fff; }
|
|
.status-4xx { color: #ffa500; }
|
|
.status-5xx { color: #ff5252; }
|
|
.send-btn {
|
|
background: #0054e6;
|
|
color: #fff;
|
|
border: none;
|
|
border-radius: 8px;
|
|
padding: 0 20px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex; align-items: center; gap: 6px;
|
|
transition: background .2s;
|
|
white-space: nowrap;
|
|
height: 40px;
|
|
}
|
|
.send-btn:hover { background: #0043b8; }
|
|
.send-btn:disabled { background: #0054e680; cursor: not-allowed; }
|
|
.url-input {
|
|
flex: 1;
|
|
background: var(--surface-800);
|
|
border: 1px solid var(--surface-600);
|
|
border-radius: 8px;
|
|
padding: 0 12px;
|
|
font-size: 13px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: var(--select-color);
|
|
outline: none;
|
|
height: 40px;
|
|
}
|
|
.url-input:focus { border-color: rgba(0,84,230,.5); }
|
|
.url-input::placeholder { color: var(--text-muted); }
|
|
select.method-select {
|
|
background: var(--surface-800);
|
|
border: 1px solid var(--surface-600);
|
|
border-radius: 8px;
|
|
padding: 0 10px;
|
|
font-size: 12px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-weight: 700;
|
|
color: var(--select-color);
|
|
outline: none;
|
|
cursor: pointer;
|
|
height: 40px;
|
|
}
|
|
select.method-select:focus { border-color: rgba(0,84,230,.5); }
|
|
.response-body-pre {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
padding: 12px;
|
|
margin: 0;
|
|
min-height: 200px;
|
|
overflow: auto;
|
|
color: var(--text-secondary);
|
|
}
|
|
.hljs { background: transparent !important; padding: 0 !important; }
|
|
.history-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 7px 10px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: background .15s;
|
|
}
|
|
.history-item:hover { background: rgba(255,255,255,.04); }
|
|
.history-url {
|
|
flex: 1;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.history-meta {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 10px;
|
|
color: var(--text-muted);
|
|
white-space: nowrap;
|
|
}
|
|
.body-type-select {
|
|
background: var(--surface-800);
|
|
border: 1px solid var(--surface-600);
|
|
border-radius: 6px;
|
|
padding: 4px 8px;
|
|
font-size: 11px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: var(--select-color);
|
|
outline: none;
|
|
cursor: pointer;
|
|
}
|
|
textarea.body-area {
|
|
width: 100%;
|
|
background: var(--surface-800);
|
|
border: 1px solid var(--surface-600);
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
font-size: 12px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: var(--select-color);
|
|
outline: none;
|
|
resize: vertical;
|
|
min-height: 160px;
|
|
line-height: 1.6;
|
|
transition: border-color .2s;
|
|
}
|
|
textarea.body-area:focus { border-color: rgba(0,84,230,.5); }
|
|
textarea.body-area::placeholder { color: var(--text-muted); }
|
|
.spinner {
|
|
width: 16px; height: 16px;
|
|
border: 2px solid rgba(255,255,255,.3);
|
|
border-top-color: #fff;
|
|
border-radius: 50%;
|
|
animation: spin .6s linear infinite;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.panels-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
@media(max-width: 900px) {
|
|
.panels-grid { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
|
|
|
<!-- Sidebar -->
|
|
<nav class="sidebar">
|
|
<div class="sidebar-logo">WA</div>
|
|
<div class="sidebar-nav">
|
|
<a href="/" class="sidebar-link" title="Главная">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
|
|
</a>
|
|
<a href="/compress" class="sidebar-link" title="Картинки">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
|
</a>
|
|
<a href="/md" class="sidebar-link" title="Markdown">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
|
</a>
|
|
<a href="/placeholder" class="sidebar-link" title="Placeholder">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
|
|
</a>
|
|
<a href="/parser" class="sidebar-link" title="Parser">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
|
|
</a>
|
|
<a href="/password" class="sidebar-link" title="Password">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
|
|
</a>
|
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
|
|
</a>
|
|
<a href="/converter" class="sidebar-link" title="Converter">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
|
|
</a>
|
|
<a href="/formatter" class="sidebar-link" title="Formatter">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
|
|
</a>
|
|
<a href="/editor" class="sidebar-link" title="Editor">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
|
</a>
|
|
<a href="/httpclient" class="sidebar-link active" title="HTTP Client">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
|
</a>
|
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
|
</a>
|
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
|
</a>
|
|
</div>
|
|
<div class="sidebar-bottom">
|
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
|
<div class="theme-toggle-knob">
|
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="main-content">
|
|
<div class="relative z-10 max-w-7xl mx-auto px-4 py-6">
|
|
|
|
<!-- Header -->
|
|
<div class="mb-8 text-center">
|
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
|
GET · POST · PUT · DELETE
|
|
</div>
|
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
|
HTTP <span class="text-accent">Клиент</span>
|
|
</h1>
|
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">тест API запросов</p>
|
|
</div>
|
|
|
|
<!-- URL Bar -->
|
|
<div class="flex gap-2 mb-4">
|
|
<select id="method" class="method-select">
|
|
<option value="GET">GET</option>
|
|
<option value="POST">POST</option>
|
|
<option value="PUT">PUT</option>
|
|
<option value="PATCH">PATCH</option>
|
|
<option value="DELETE">DELETE</option>
|
|
<option value="HEAD">HEAD</option>
|
|
<option value="OPTIONS">OPTIONS</option>
|
|
</select>
|
|
<input id="urlInput" type="text" class="url-input" placeholder="https://api.example.com/endpoint" autocomplete="off" spellcheck="false" />
|
|
<button id="sendBtn" class="send-btn">
|
|
<svg width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
|
Отправить
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Panels -->
|
|
<div class="panels-grid mb-4">
|
|
|
|
<!-- REQUEST PANEL -->
|
|
<div class="panel">
|
|
<div class="flex border-b border-surface-600">
|
|
<button class="tab-btn active" data-reqtab="params">Параметры</button>
|
|
<button class="tab-btn" data-reqtab="headers">Заголовки</button>
|
|
<button class="tab-btn" data-reqtab="body">Тело</button>
|
|
<button class="tab-btn" data-reqtab="auth">Авторизация</button>
|
|
</div>
|
|
|
|
<!-- Params tab -->
|
|
<div id="reqTab-params" class="p-3">
|
|
<div id="params-rows"></div>
|
|
<div class="kv-add" id="params-add">+ Добавить параметр</div>
|
|
</div>
|
|
|
|
<!-- Headers tab -->
|
|
<div id="reqTab-headers" class="p-3 hidden">
|
|
<div id="headers-rows"></div>
|
|
<div class="kv-add" id="headers-add">+ Добавить заголовок</div>
|
|
</div>
|
|
|
|
<!-- Body tab -->
|
|
<div id="reqTab-body" class="p-3 hidden">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="text-xs font-mono text-muted">Тип:</span>
|
|
<select id="bodyType" class="body-type-select">
|
|
<option value="none">нет</option>
|
|
<option value="json">JSON</option>
|
|
<option value="form">form-urlencoded</option>
|
|
<option value="raw">текст</option>
|
|
</select>
|
|
</div>
|
|
<div id="body-none" class="text-xs font-mono text-muted py-4 text-center">Тело запроса не отправляется</div>
|
|
<div id="body-json" class="hidden">
|
|
<textarea id="bodyJson" class="body-area" placeholder='{"key": "value"}'></textarea>
|
|
</div>
|
|
<div id="body-form" class="hidden">
|
|
<div id="form-rows"></div>
|
|
<div class="kv-add" id="form-add">+ Добавить поле</div>
|
|
</div>
|
|
<div id="body-raw" class="hidden">
|
|
<textarea id="bodyRaw" class="body-area" placeholder="Raw request body..."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Auth tab -->
|
|
<div id="reqTab-auth" class="p-3 hidden">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="text-xs font-mono text-muted">Тип:</span>
|
|
<select id="authType" class="body-type-select">
|
|
<option value="none">нет</option>
|
|
<option value="bearer">Bearer Токен</option>
|
|
<option value="basic">Basic Auth</option>
|
|
</select>
|
|
</div>
|
|
<div id="auth-none" class="text-xs font-mono text-muted py-4 text-center">Без авторизации</div>
|
|
<div id="auth-bearer" class="hidden">
|
|
<input id="bearerToken" type="text" class="kv-row" style="width:100%;display:block;margin:0;border-radius:8px;padding:8px 12px;background:var(--select-bg);border:1px solid var(--surface-600);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--select-color);outline:none" placeholder="Токен..." />
|
|
</div>
|
|
<div id="auth-basic" class="hidden">
|
|
<input id="basicUser" type="text" style="width:100%;display:block;margin-bottom:8px;border-radius:8px;padding:8px 12px;background:var(--select-bg);border:1px solid var(--surface-600);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--select-color);outline:none" placeholder="Логин" />
|
|
<input id="basicPass" type="password" style="width:100%;display:block;border-radius:8px;padding:8px 12px;background:var(--select-bg);border:1px solid var(--surface-600);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--select-color);outline:none" placeholder="Пароль" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RESPONSE PANEL -->
|
|
<div class="panel">
|
|
<!-- Status bar -->
|
|
<div id="responseStatusBar" class="flex items-center gap-3 px-4 py-2 border-b border-surface-600 min-h-[41px]">
|
|
<span class="text-xs font-mono text-muted">Нажмите «Отправить» для запроса</span>
|
|
</div>
|
|
<!-- Tabs -->
|
|
<div class="flex border-b border-surface-600">
|
|
<button class="tab-btn active" data-restab="body">Тело</button>
|
|
<button class="tab-btn" data-restab="headers">Заголовки</button>
|
|
</div>
|
|
<!-- Body -->
|
|
<div id="resTab-body">
|
|
<div id="responseBodyWrap" style="max-height:480px;overflow:auto;position:relative">
|
|
<div id="responseBodyEmpty" class="flex flex-col items-center justify-center py-16 text-center">
|
|
<svg width="32" height="32" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1" class="mb-3 opacity-20"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
|
<span class="text-xs font-mono text-muted">Ответ появится здесь</span>
|
|
</div>
|
|
<pre id="responseBodyPre" class="response-body-pre hidden"><code id="responseBodyCode"></code></pre>
|
|
</div>
|
|
<div id="responseCopyBar" class="hidden flex items-center justify-between px-3 py-1.5 border-t border-surface-600">
|
|
<span id="responseSizeLabel" class="text-xs font-mono text-muted"></span>
|
|
<button id="copyBodyBtn" class="text-xs font-mono text-muted hover:text-accent transition-colors">Копировать</button>
|
|
</div>
|
|
</div>
|
|
<!-- Response Headers -->
|
|
<div id="resTab-headers" class="hidden p-3" style="max-height:480px;overflow:auto">
|
|
<div id="responseHeadersContent" class="text-xs font-mono space-y-1"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- History -->
|
|
<div class="panel">
|
|
<div class="flex items-center justify-between px-4 py-2 border-b border-surface-600 cursor-pointer select-none" id="historyToggle">
|
|
<span class="text-xs font-mono font-semibold" style="color:var(--text-secondary)">История запросов</span>
|
|
<div class="flex items-center gap-3">
|
|
<button id="clearHistoryBtn" class="text-xs font-mono text-muted hover:text-danger transition-colors hidden">Очистить</button>
|
|
<svg id="historyArrow" width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" style="color:var(--text-muted);transition:transform .2s"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/></svg>
|
|
</div>
|
|
</div>
|
|
<div id="historyList" class="px-2 py-1" style="max-height:260px;overflow-y:auto"></div>
|
|
</div>
|
|
|
|
</div>
|
|
</div><!-- /main-content -->
|
|
|
|
<script src="/vendor/highlight.min.js"></script>
|
|
<script>
|
|
(() => {
|
|
// Theme
|
|
const html = document.documentElement;
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme) html.className = savedTheme;
|
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) html.className = '';
|
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
|
const isDark = html.classList.toggle('dark');
|
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
|
});
|
|
|
|
// State
|
|
let reqTabs = {}, resTabs = {};
|
|
let isLoading = false;
|
|
|
|
// KV row builder
|
|
function makeKvRow(container, keyVal, valVal) {
|
|
const row = document.createElement('div');
|
|
row.className = 'kv-row';
|
|
const k = document.createElement('input');
|
|
k.type = 'text'; k.placeholder = 'Ключ'; k.value = keyVal || '';
|
|
k.className = 'kv-input-key';
|
|
const v = document.createElement('input');
|
|
v.type = 'text'; v.placeholder = 'Значение'; v.value = valVal || '';
|
|
v.className = 'kv-input-val';
|
|
const del = document.createElement('div');
|
|
del.className = 'kv-del';
|
|
del.innerHTML = '<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>';
|
|
del.onclick = () => row.remove();
|
|
row.appendChild(k); row.appendChild(v); row.appendChild(del);
|
|
container.appendChild(row);
|
|
return row;
|
|
}
|
|
|
|
function getKvRows(container) {
|
|
const result = {};
|
|
container.querySelectorAll('.kv-row').forEach(row => {
|
|
const k = row.querySelector('.kv-input-key')?.value?.trim();
|
|
const v = row.querySelector('.kv-input-val')?.value?.trim();
|
|
if (k) result[k] = v || '';
|
|
});
|
|
return result;
|
|
}
|
|
|
|
// Init KV containers
|
|
const paramsRows = document.getElementById('params-rows');
|
|
const headersRows = document.getElementById('headers-rows');
|
|
const formRows = document.getElementById('form-rows');
|
|
makeKvRow(paramsRows, '', '');
|
|
makeKvRow(headersRows, 'Content-Type', 'application/json');
|
|
makeKvRow(formRows, '', '');
|
|
|
|
document.getElementById('params-add').onclick = () => makeKvRow(paramsRows, '', '');
|
|
document.getElementById('headers-add').onclick = () => makeKvRow(headersRows, '', '');
|
|
document.getElementById('form-add').onclick = () => makeKvRow(formRows, '', '');
|
|
|
|
// Request tabs
|
|
document.querySelectorAll('[data-reqtab]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('[data-reqtab]').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
const tab = btn.dataset.reqtab;
|
|
['params','headers','body','auth'].forEach(t => {
|
|
document.getElementById('reqTab-' + t).classList.toggle('hidden', t !== tab);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Response tabs
|
|
document.querySelectorAll('[data-restab]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('[data-restab]').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
const tab = btn.dataset.restab;
|
|
['body','headers'].forEach(t => {
|
|
document.getElementById('resTab-' + t).classList.toggle('hidden', t !== tab);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Body type
|
|
document.getElementById('bodyType').addEventListener('change', function() {
|
|
['none','json','form','raw'].forEach(t => {
|
|
document.getElementById('body-' + t).classList.toggle('hidden', t !== this.value);
|
|
});
|
|
});
|
|
|
|
// Auth type
|
|
document.getElementById('authType').addEventListener('change', function() {
|
|
['none','bearer','basic'].forEach(t => {
|
|
document.getElementById('auth-' + t).classList.toggle('hidden', t !== this.value);
|
|
});
|
|
});
|
|
|
|
// Build request config
|
|
function buildRequest() {
|
|
const method = document.getElementById('method').value;
|
|
let url = document.getElementById('urlInput').value.trim();
|
|
if (!url) return null;
|
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
url = 'https://' + url;
|
|
document.getElementById('urlInput').value = url;
|
|
}
|
|
|
|
// Params → append to URL
|
|
const params = getKvRows(paramsRows);
|
|
if (Object.keys(params).length > 0) {
|
|
try {
|
|
const u = new URL(url);
|
|
Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v));
|
|
url = u.toString();
|
|
} catch {}
|
|
}
|
|
|
|
// Headers
|
|
const headers = getKvRows(headersRows);
|
|
|
|
// Auth
|
|
const authType = document.getElementById('authType').value;
|
|
if (authType === 'bearer') {
|
|
const tok = document.getElementById('bearerToken').value.trim();
|
|
if (tok) headers['Authorization'] = 'Bearer ' + tok;
|
|
} else if (authType === 'basic') {
|
|
const u = document.getElementById('basicUser').value;
|
|
const p = document.getElementById('basicPass').value;
|
|
headers['Authorization'] = 'Basic ' + btoa(u + ':' + p);
|
|
}
|
|
|
|
// Body
|
|
let body = null;
|
|
const bodyType = document.getElementById('bodyType').value;
|
|
if (!['GET','HEAD'].includes(method)) {
|
|
if (bodyType === 'json') {
|
|
body = document.getElementById('bodyJson').value.trim();
|
|
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
} else if (bodyType === 'form') {
|
|
const formData = getKvRows(formRows);
|
|
body = new URLSearchParams(formData).toString();
|
|
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
} else if (bodyType === 'raw') {
|
|
body = document.getElementById('bodyRaw').value;
|
|
}
|
|
}
|
|
|
|
return { method, url, headers, body };
|
|
}
|
|
|
|
// Send
|
|
async function sendRequest() {
|
|
if (isLoading) return;
|
|
const req = buildRequest();
|
|
if (!req) {
|
|
document.getElementById('urlInput').focus();
|
|
return;
|
|
}
|
|
|
|
isLoading = true;
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
sendBtn.disabled = true;
|
|
sendBtn.innerHTML = '<div class="spinner"></div> Отправка...';
|
|
|
|
// Status bar: loading
|
|
const statusBar = document.getElementById('responseStatusBar');
|
|
statusBar.innerHTML = '<span class="text-xs font-mono text-muted">Отправка запроса...</span>';
|
|
|
|
// Hide response
|
|
document.getElementById('responseBodyEmpty').classList.remove('hidden');
|
|
document.getElementById('responseBodyPre').classList.add('hidden');
|
|
document.getElementById('responseCopyBar').classList.add('hidden');
|
|
document.getElementById('responseHeadersContent').innerHTML = '';
|
|
|
|
try {
|
|
const t0 = performance.now();
|
|
const resp = await fetch('/api/proxy', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(req),
|
|
});
|
|
const data = await resp.json();
|
|
const elapsed = Math.round(performance.now() - t0);
|
|
|
|
if (data.error && !data.status) {
|
|
// Network/timeout error
|
|
statusBar.innerHTML = `<span class="text-xs font-mono" style="color:#ff5252">Ошибка: ${escHtml(data.error)}</span><span class="text-xs font-mono text-muted ml-auto">${data.time || elapsed}мс</span>`;
|
|
showBody('❌ ' + data.error, 'plaintext');
|
|
addToHistory(req.method, req.url, null, data.time || elapsed);
|
|
return;
|
|
}
|
|
|
|
// Status badge
|
|
const statusClass = data.status >= 500 ? 'status-5xx' : data.status >= 400 ? 'status-4xx' : data.status >= 300 ? 'status-3xx' : 'status-2xx';
|
|
const sizeStr = formatSize(data.size);
|
|
statusBar.innerHTML = `
|
|
<span class="text-sm font-mono font-bold ${statusClass}">${data.status} ${escHtml(data.statusText || '')}</span>
|
|
<span class="text-xs font-mono text-muted">${data.time}мс</span>
|
|
<span class="text-xs font-mono text-muted">${sizeStr}</span>
|
|
${data.truncated ? '<span class="text-xs font-mono" style="color:#ffa500">⚠ обрезано до 10MB</span>' : ''}
|
|
${data.url !== req.url ? '<span class="text-xs font-mono text-muted ml-auto" title="' + escHtml(data.url) + '">→ redirect</span>' : ''}
|
|
`;
|
|
|
|
// Response body
|
|
const ct = (data.headers?.['content-type'] || '').toLowerCase();
|
|
let lang = 'plaintext';
|
|
let bodyText = data.body || '';
|
|
if (ct.includes('json')) {
|
|
lang = 'json';
|
|
try { bodyText = JSON.stringify(JSON.parse(bodyText), null, 2); } catch {}
|
|
} else if (ct.includes('html')) {
|
|
lang = 'xml';
|
|
} else if (ct.includes('xml')) {
|
|
lang = 'xml';
|
|
} else if (ct.includes('javascript')) {
|
|
lang = 'javascript';
|
|
}
|
|
showBody(bodyText, lang);
|
|
|
|
// Response headers
|
|
const hContent = document.getElementById('responseHeadersContent');
|
|
hContent.innerHTML = '';
|
|
if (data.headers) {
|
|
Object.entries(data.headers).sort().forEach(([k, v]) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'flex gap-2 py-0.5';
|
|
row.innerHTML = `<span style="color:#6b9fff;min-width:160px">${escHtml(k)}</span><span style="color:var(--text-secondary)">${escHtml(v)}</span>`;
|
|
hContent.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Copy button
|
|
const copyBar = document.getElementById('responseCopyBar');
|
|
copyBar.classList.remove('hidden');
|
|
document.getElementById('responseSizeLabel').textContent = sizeStr;
|
|
document.getElementById('copyBodyBtn').onclick = () => {
|
|
navigator.clipboard.writeText(data.body || '').then(() => {
|
|
document.getElementById('copyBodyBtn').textContent = 'Скопировано!';
|
|
setTimeout(() => document.getElementById('copyBodyBtn').textContent = 'Копировать', 1500);
|
|
});
|
|
};
|
|
|
|
addToHistory(req.method, req.url, data.status, data.time);
|
|
} catch (err) {
|
|
statusBar.innerHTML = `<span class="text-xs font-mono" style="color:#ff5252">Ошибка соединения: ${escHtml(err.message)}</span>`;
|
|
showBody('Ошибка: ' + err.message, 'plaintext');
|
|
} finally {
|
|
isLoading = false;
|
|
sendBtn.disabled = false;
|
|
sendBtn.innerHTML = '<svg width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg> Отправить';
|
|
}
|
|
}
|
|
|
|
function showBody(text, lang) {
|
|
const empty = document.getElementById('responseBodyEmpty');
|
|
const pre = document.getElementById('responseBodyPre');
|
|
const code = document.getElementById('responseBodyCode');
|
|
empty.classList.add('hidden');
|
|
pre.classList.remove('hidden');
|
|
code.textContent = text;
|
|
code.className = 'language-' + lang;
|
|
if (window.hljs && lang !== 'plaintext') {
|
|
hljs.highlightElement(code);
|
|
}
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (!bytes) return '0 B';
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB';
|
|
return (bytes/(1024*1024)).toFixed(1) + ' MB';
|
|
}
|
|
|
|
function escHtml(s) {
|
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// Send button + Enter
|
|
document.getElementById('sendBtn').onclick = sendRequest;
|
|
document.getElementById('urlInput').addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') sendRequest();
|
|
});
|
|
|
|
// History (server-side, per session)
|
|
const methodColors = { GET:'#00a86b', POST:'#ffa500', PUT:'#6b9fff', PATCH:'#cc66ff', DELETE:'#ff5252', HEAD:'#aaa', OPTIONS:'#aaa' };
|
|
|
|
async function addToHistory(method, url, status, time) {
|
|
await fetch('/api/history/add', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ method, url, status, time, ts: Date.now() }),
|
|
}).catch(() => {});
|
|
renderHistory();
|
|
}
|
|
|
|
async function renderHistory() {
|
|
const list = document.getElementById('historyList');
|
|
const clearBtn = document.getElementById('clearHistoryBtn');
|
|
let h = [];
|
|
try {
|
|
const r = await fetch('/api/history');
|
|
h = await r.json();
|
|
} catch {}
|
|
if (!Array.isArray(h) || h.length === 0) {
|
|
list.innerHTML = '<div class="text-xs font-mono text-muted py-4 text-center">История пуста</div>';
|
|
clearBtn.classList.add('hidden');
|
|
return;
|
|
}
|
|
clearBtn.classList.remove('hidden');
|
|
list.innerHTML = '';
|
|
h.forEach((item) => {
|
|
const div = document.createElement('div');
|
|
div.className = 'history-item';
|
|
const statusClass = !item.status ? 'text-muted' : item.status >= 500 ? 'status-5xx' : item.status >= 400 ? 'status-4xx' : item.status >= 300 ? 'status-3xx' : 'status-2xx';
|
|
const statusText = item.status ? item.status : '—';
|
|
div.innerHTML = `
|
|
<span class="method-badge m-${item.method}" style="min-width:56px;justify-content:center">${item.method}</span>
|
|
<span class="history-url">${escHtml(item.url)}</span>
|
|
<span class="history-meta ${statusClass}">${statusText}</span>
|
|
<span class="history-meta">${item.time}мс</span>
|
|
`;
|
|
div.onclick = () => loadHistoryItem(item);
|
|
list.appendChild(div);
|
|
});
|
|
}
|
|
|
|
function loadHistoryItem(item) {
|
|
document.getElementById('method').value = item.method;
|
|
document.getElementById('urlInput').value = item.url;
|
|
}
|
|
|
|
document.getElementById('clearHistoryBtn').onclick = async (e) => {
|
|
e.stopPropagation();
|
|
await fetch('/api/history', { method: 'DELETE' }).catch(() => {});
|
|
renderHistory();
|
|
};
|
|
|
|
// History toggle
|
|
let historyOpen = true;
|
|
document.getElementById('historyToggle').onclick = () => {
|
|
historyOpen = !historyOpen;
|
|
document.getElementById('historyList').style.display = historyOpen ? '' : 'none';
|
|
document.getElementById('historyArrow').style.transform = historyOpen ? '' : 'rotate(-90deg)';
|
|
};
|
|
|
|
renderHistory();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|