commit 75fb753fc0eefa3b1a8938be0711f7fa6ac7e6e9 Author: Patrick <147879351+WinniePatGG@users.noreply.github.com> Date: Fri May 1 20:02:13 2026 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f5657d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +node_modules +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0527b6b --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Ach zu faul \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..84c804e --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "winnieapi-v2", + "version": "1.0.0", + "description": "A sleek dark-mode utilities website", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "nanoid": "^3.3.7" + } +} + diff --git a/public/css/styles.css b/public/css/styles.css new file mode 100644 index 0000000..f3821e1 --- /dev/null +++ b/public/css/styles.css @@ -0,0 +1,482 @@ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +:root { + --bg-primary: #0a0a0f; + --bg-secondary: #12121a; + --bg-card: #16161f; + --bg-input: #1a1a25; + --border: #2a2a3a; + --border-glow: #6c63ff40; + --text-primary: #e8e8f0; + --text-secondary: #8888a0; + --text-muted: #555570; + --accent: #6c63ff; + --accent-hover: #7f78ff; + --accent-glow: #6c63ff30; + --green: #22c55e; + --red: #ef4444; + --orange: #f59e0b; + --cyan: #06b6d4; + --pink: #ec4899; + --yellow: #eab308; + --radius: 12px; + --radius-sm: 8px; + --font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace; + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} +body { + font-family: var(--font-sans); + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + overflow-x: hidden; +} +/* ─── Background Glow ─────────────────────────────────── */ +.bg-glow { position: fixed; inset: 0; z-index: -1; overflow: hidden; } +.bg-glow::before, .bg-glow::after { + content: ''; position: absolute; width: 600px; height: 600px; + border-radius: 50%; filter: blur(120px); opacity: 0.07; + animation: float 20s ease-in-out infinite; +} +.bg-glow::before { background: var(--accent); top: -200px; left: -100px; } +.bg-glow::after { background: var(--pink); bottom: -200px; right: -100px; animation-delay: -10s; } +@keyframes float { + 0%, 100% { transform: translate(0, 0) scale(1); } + 33% { transform: translate(100px, 50px) scale(1.1); } + 66% { transform: translate(-50px, -30px) scale(0.95); } +} +/* ─── Particles ─────────────────────────────────────── */ +.particles { position: fixed; inset: 0; z-index: -1; pointer-events: none; } +.particle { + position: absolute; width: 2px; height: 2px; + background: var(--accent); border-radius: 50%; opacity: 0; + animation: particle-float 15s linear infinite; +} +@keyframes particle-float { + 0% { opacity: 0; transform: translateY(100vh) scale(0); } + 10% { opacity: 0.6; } + 90% { opacity: 0.6; } + 100% { opacity: 0; transform: translateY(-10vh) scale(1); } +} +/* ─── Header ──────────────────────────────────────────── */ +header { + position: sticky; top: 0; z-index: 100; + background: rgba(10, 10, 15, 0.85); + backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 0 24px; +} +.header-inner { + max-width: 1400px; margin: 0 auto; + display: flex; align-items: center; justify-content: space-between; + height: 64px; gap: 16px; +} +.logo { + font-size: 1.35rem; font-weight: 800; + background: linear-gradient(135deg, var(--accent), var(--pink)); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; + cursor: pointer; letter-spacing: -0.5px; white-space: nowrap; +} +.logo span { opacity: 0.5; font-weight: 400; } +.search-bar { + flex: 1; max-width: 400px; position: relative; +} +.search-bar input { + width: 100%; background: var(--bg-input); border: 1px solid var(--border); + border-radius: 24px; color: var(--text-primary); + padding: 8px 16px 8px 36px; font-size: 0.85rem; + transition: var(--transition); outline: none; font-family: var(--font-sans); +} +.search-bar input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } +.search-bar i { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text-muted); font-size: 0.8rem; } +.header-actions { display: flex; gap: 8px; align-items: center; } +.header-actions button { + background: var(--bg-input); border: 1px solid var(--border); color: var(--text-secondary); + width: 36px; height: 36px; border-radius: 50%; cursor: pointer; + transition: var(--transition); display: flex; align-items: center; justify-content: center; + font-size: 0.85rem; +} +.header-actions button:hover { border-color: var(--accent); color: var(--accent); } +/* ─── Main ────────────────────────────────────────────── */ +main { max-width: 1400px; margin: 0 auto; padding: 32px 24px 80px; } +.page { display: none; animation: fadeIn 0.35s ease; } +.page.active { display: block; } +@keyframes fadeIn { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} +/* ─── Hero ────────────────────────────────────────────── */ +.hero { text-align: center; padding: 60px 0 48px; } +.hero h1 { + font-size: clamp(2.2rem, 5vw, 3.5rem); + font-weight: 800; letter-spacing: -1.5px; line-height: 1.1; margin-bottom: 14px; +} +.hero h1 .gradient { + background: linear-gradient(135deg, var(--accent), var(--cyan), var(--pink)); + -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; +} +.hero p { color: var(--text-secondary); font-size: 1.1rem; max-width: 520px; margin: 0 auto 12px; line-height: 1.6; } +.hero .stats { + display: flex; gap: 32px; justify-content: center; margin-top: 20px; +} +.hero .stat { text-align: center; } +.hero .stat-num { font-size: 1.8rem; font-weight: 800; color: var(--accent); } +.hero .stat-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; } +/* ─── Category Tabs ───────────────────────────────────── */ +.categories { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; margin-bottom: 28px; } +.cat-btn { + background: var(--bg-input); border: 1px solid var(--border); color: var(--text-secondary); + padding: 6px 16px; border-radius: 20px; font-size: 0.8rem; font-weight: 500; + cursor: pointer; transition: var(--transition); font-family: var(--font-sans); +} +.cat-btn:hover { border-color: var(--accent); color: var(--text-primary); } +.cat-btn.active { background: var(--accent-glow); border-color: var(--accent); color: var(--accent); } +/* ─── Tools Grid ──────────────────────────────────────── */ +.tools-grid { + display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; +} +.tool-card { + background: var(--bg-card); border: 1px solid var(--border); + border-radius: var(--radius); padding: 24px; cursor: pointer; + transition: var(--transition); position: relative; overflow: hidden; +} +.tool-card::before { + content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; + opacity: 0; transition: var(--transition); +} +.tool-card:hover { + border-color: var(--border-glow); transform: translateY(-3px); + box-shadow: 0 16px 32px rgba(0,0,0,0.3), 0 0 20px var(--accent-glow); +} +.tool-card:hover::before { opacity: 1; } +.tool-card .tc-top { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; } +.tool-card .icon { + width: 42px; height: 42px; border-radius: var(--radius-sm); + display: flex; align-items: center; justify-content: center; + font-size: 1.1rem; flex-shrink: 0; +} +.tool-card h3 { font-size: 1rem; font-weight: 700; } +.tool-card p { color: var(--text-secondary); font-size: 0.82rem; line-height: 1.5; } +.tool-card .tag { + display: inline-block; padding: 2px 8px; border-radius: 20px; + font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; + margin-left: auto; +} +/* Icon colors */ +.ic-purple { background: #6c63ff15; color: var(--accent); } +.ic-green { background: #22c55e15; color: var(--green); } +.ic-red { background: #ef444415; color: var(--red); } +.ic-cyan { background: #06b6d415; color: var(--cyan); } +.ic-orange { background: #f59e0b15; color: var(--orange); } +.ic-pink { background: #ec489915; color: var(--pink); } +.ic-yellow { background: #eab30815; color: var(--yellow); } +/* Gradient bars */ +[data-cat="format"]::before { background: linear-gradient(90deg, var(--accent), var(--cyan)); } +[data-cat="encode"]::before { background: linear-gradient(90deg, var(--green), var(--cyan)); } +[data-cat="generate"]::before{ background: linear-gradient(90deg, var(--orange), var(--yellow)); } +[data-cat="text"]::before { background: linear-gradient(90deg, var(--pink), var(--accent)); } +[data-cat="web"]::before { background: linear-gradient(90deg, var(--cyan), var(--green)); } +[data-cat="dev"]::before { background: linear-gradient(90deg, var(--red), var(--orange)); } +.tag-purple { background: #6c63ff20; color: var(--accent); } +.tag-green { background: #22c55e20; color: var(--green); } +.tag-red { background: #ef444420; color: var(--red); } +.tag-cyan { background: #06b6d420; color: var(--cyan); } +.tag-orange { background: #f59e0b20; color: var(--orange); } +.tag-pink { background: #ec489920; color: var(--pink); } +.tag-yellow { background: #eab30820; color: var(--yellow); } +/* ─── Section Header / Back ───────────────────────────── */ +.section-header { margin-bottom: 24px; } +.section-header h2 { font-size: 1.6rem; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 4px; } +.section-header p { color: var(--text-secondary); font-size: 0.9rem; } +.back-btn { + background: transparent; border: 1px solid var(--border); + color: var(--text-secondary); padding: 6px 14px; border-radius: var(--radius-sm); + cursor: pointer; font-size: 0.8rem; margin-bottom: 16px; + transition: var(--transition); font-family: var(--font-sans); +} +.back-btn:hover { border-color: var(--accent); color: var(--accent); } +/* ─── Form Controls ───────────────────────────────────── */ +textarea, input[type="text"], input[type="url"], input[type="number"], input[type="password"], select { + width: 100%; background: var(--bg-input); border: 1px solid var(--border); + border-radius: var(--radius-sm); color: var(--text-primary); + padding: 12px 16px; font-family: var(--font-mono); font-size: 0.85rem; + transition: var(--transition); outline: none; +} +textarea:focus, input:focus, select:focus { + border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); +} +textarea { resize: vertical; min-height: 180px; line-height: 1.6; } +input[type="color"] { + width: 60px; height: 42px; border: 1px solid var(--border); + border-radius: var(--radius-sm); background: var(--bg-input); + cursor: pointer; padding: 4px; +} +.btn { + display: inline-flex; align-items: center; gap: 8px; + padding: 10px 20px; border: none; border-radius: var(--radius-sm); + font-family: var(--font-sans); font-size: 0.85rem; font-weight: 600; + cursor: pointer; transition: var(--transition); white-space: nowrap; +} +.btn-primary { background: var(--accent); color: white; } +.btn-primary:hover { background: var(--accent-hover); box-shadow: 0 4px 20px var(--accent-glow); } +.btn-secondary { background: var(--bg-input); color: var(--text-secondary); border: 1px solid var(--border); } +.btn-secondary:hover { border-color: var(--accent); color: var(--text-primary); } +.btn-green { background: var(--green); color: white; } +.btn-green:hover { background: #16a34a; } +.btn-red { background: var(--red); color: white; } +.btn-red:hover { background: #dc2626; } +.btn-cyan { background: var(--cyan); color: white; } +.btn-cyan:hover { background: #0891b2; } +.btn-orange { background: var(--orange); color: #000; } +.btn-orange:hover { background: #d97706; } +.btn-pink { background: var(--pink); color: white; } +.btn-pink:hover { background: #db2777; } +.btn-sm { padding: 6px 14px; font-size: 0.78rem; } +.btn-group { display: flex; gap: 8px; flex-wrap: wrap; margin: 14px 0; } +/* ─── Panels ──────────────────────────────────────────── */ +.split-panel { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } +.panel-label { + font-size: 0.72rem; font-weight: 600; text-transform: uppercase; + letter-spacing: 1px; color: var(--text-muted); margin-bottom: 8px; +} +/* ─── Result Cards ────────────────────────────────────── */ +.result-card { + background: var(--bg-card); border: 1px solid var(--border); + border-radius: var(--radius); padding: 20px; margin-top: 16px; +} +.result-row { + display: flex; justify-content: space-between; align-items: center; + padding: 10px 14px; background: var(--bg-input); + border-radius: var(--radius-sm); margin-bottom: 6px; + border: 1px solid var(--border); +} +.result-row .label { font-size: 0.72rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; } +.result-row .value { + font-family: var(--font-mono); font-size: 0.82rem; color: var(--cyan); + word-break: break-all; cursor: pointer; text-align: right; max-width: 70%; +} +.result-row .value:hover { color: var(--accent); } +/* ─── Status ──────────────────────────────────────────── */ +.status { + padding: 10px 14px; border-radius: var(--radius-sm); font-size: 0.82rem; + margin-top: 10px; display: none; font-family: var(--font-mono); +} +.status.success { display: block; background: #22c55e12; border: 1px solid #22c55e30; color: var(--green); } +.status.error { display: block; background: #ef444412; border: 1px solid #ef444430; color: var(--red); } +.status.info { display: block; background: #6c63ff12; border: 1px solid #6c63ff30; color: var(--accent); } +.status.warning { display: block; background: #f59e0b12; border: 1px solid #f59e0b30; color: var(--orange); } +/* ─── Short URL ───────────────────────────────────────── */ +.short-url-result { display: none; margin-top: 16px; } +.short-url-result.visible { display: block; animation: fadeIn 0.35s ease; } +.short-url-display { + display: flex; align-items: center; gap: 12px; + background: var(--bg-input); border: 1px solid var(--accent); + border-radius: var(--radius-sm); padding: 14px 16px; margin: 10px 0; +} +.short-url-display a { flex: 1; color: var(--accent); font-family: var(--font-mono); font-size: 0.9rem; text-decoration: none; } +.short-url-display a:hover { text-decoration: underline; } +/* ─── YouTube Card ────────────────────────────────────── */ +.yt-result { display: none; overflow: hidden; margin-top: 16px; } +.yt-result.visible { display: block; animation: fadeIn 0.35s ease; } +.yt-thumb { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block; border-radius: var(--radius) var(--radius) 0 0; } +.yt-info { padding: 20px; background: var(--bg-card); border: 1px solid var(--border); border-top: 0; border-radius: 0 0 var(--radius) var(--radius); } +.yt-info h3 { font-size: 1.1rem; margin-bottom: 6px; line-height: 1.4; } +.yt-info .author { color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 14px; } +.yt-info .author a { color: var(--accent); text-decoration: none; } +.yt-links { display: flex; gap: 8px; flex-wrap: wrap; } +/* ─── Color Preview ───────────────────────────────────── */ +.color-preview-box { + width: 100%; height: 120px; border-radius: var(--radius); + border: 1px solid var(--border); margin-bottom: 16px; + transition: background 0.3s ease; +} +/* ─── Checkbox & Toggle ───────────────────────────────── */ +.checkbox-group { display: flex; flex-wrap: wrap; gap: 16px; margin: 12px 0; } +.checkbox-group label { + display: flex; align-items: center; gap: 6px; font-size: 0.85rem; + color: var(--text-secondary); cursor: pointer; +} +.checkbox-group input[type="checkbox"] { + width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; +} +/* ─── Password Display ────────────────────────────────── */ +.pw-display { + font-family: var(--font-mono); font-size: 1.1rem; + padding: 16px; background: var(--bg-input); + border: 1px solid var(--border); border-radius: var(--radius-sm); + word-break: break-all; cursor: pointer; + transition: var(--transition); margin: 12px 0; +} +.pw-display:hover { border-color: var(--accent); } +/* ─── Strength Meter ──────────────────────────────────── */ +.strength-bar { height: 4px; border-radius: 2px; background: var(--border); margin-top: 8px; overflow: hidden; } +.strength-bar-fill { height: 100%; border-radius: 2px; transition: all 0.3s ease; } +/* ─── Markdown Preview ────────────────────────────────── */ +.md-preview { + background: var(--bg-input); border: 1px solid var(--border); + border-radius: var(--radius-sm); padding: 20px; min-height: 180px; + line-height: 1.7; font-size: 0.9rem; overflow-y: auto; max-height: 600px; +} +.md-preview h1, .md-preview h2, .md-preview h3 { margin: 16px 0 8px; color: var(--text-primary); } +.md-preview h1 { font-size: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: 8px; } +.md-preview h2 { font-size: 1.25rem; } +.md-preview h3 { font-size: 1.1rem; } +.md-preview p { margin-bottom: 10px; color: var(--text-secondary); } +.md-preview code { + background: #1e1e2e; padding: 2px 6px; border-radius: 4px; + font-family: var(--font-mono); font-size: 0.82rem; color: var(--cyan); +} +.md-preview pre { background: #1e1e2e; padding: 14px; border-radius: var(--radius-sm); margin: 10px 0; overflow-x: auto; } +.md-preview pre code { background: none; padding: 0; } +.md-preview ul, .md-preview ol { padding-left: 24px; margin-bottom: 10px; color: var(--text-secondary); } +.md-preview blockquote { border-left: 3px solid var(--accent); padding-left: 14px; margin: 10px 0; color: var(--text-muted); font-style: italic; } +.md-preview a { color: var(--accent); } +.md-preview strong { color: var(--text-primary); } +.md-preview hr { border: none; border-top: 1px solid var(--border); margin: 16px 0; } +/* ─── Diff View ───────────────────────────────────────── */ +.diff-output { font-family: var(--font-mono); font-size: 0.82rem; margin-top: 16px; } +.diff-line { padding: 4px 12px; border-radius: 2px; margin: 1px 0; } +.diff-add { background: #22c55e10; color: var(--green); } +.diff-del { background: #ef444410; color: var(--red); } +.diff-same { color: var(--text-muted); } +/* ─── Regex Matches ───────────────────────────────────── */ +.regex-match { background: var(--accent-glow); border: 1px solid var(--accent); border-radius: 3px; padding: 0 2px; } +/* ─── IP Info ─────────────────────────────────────────── */ +.ip-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; } +.ip-card { + background: var(--bg-input); border: 1px solid var(--border); + border-radius: var(--radius-sm); padding: 14px; +} +.ip-card .label { font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; } +.ip-card .value { font-family: var(--font-mono); font-size: 0.9rem; color: var(--cyan); } +/* ─── QR Code ─────────────────────────────────────────── */ +#qrCanvas { display: inline-block; } +/* ─── Footer ──────────────────────────────────────────── */ +footer { + text-align: center; padding: 28px 24px; color: var(--text-muted); + font-size: 0.78rem; border-top: 1px solid var(--border); +} +footer a { color: var(--accent); text-decoration: none; } +/* ─── Scrollbar ───────────────────────────────────────── */ +::-webkit-scrollbar { width: 8px; } +::-webkit-scrollbar-track { background: var(--bg-primary); } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } +::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } +::selection { background: var(--accent); color: white; } +/* ─── Toast ───────────────────────────────────────────── */ +.copy-toast { + position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); + background: var(--accent); color: white; padding: 10px 24px; + border-radius: 30px; font-size: 0.85rem; font-weight: 600; + opacity: 0; pointer-events: none; transition: opacity 0.3s ease; z-index: 999; +} +.copy-toast.show { opacity: 1; } +/* ─── Responsive ──────────────────────────────────────── */ +@media (max-width: 768px) { + .split-panel { grid-template-columns: 1fr; } + .search-bar { display: none; } + .tools-grid { grid-template-columns: 1fr; } + .ip-grid { grid-template-columns: 1fr; } +} +/* ─── Slider ──────────────────────────────────────────── */ +input[type="range"] { + -webkit-appearance: none; width: 100%; height: 6px; + background: var(--border); border-radius: 3px; outline: none; +} +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; width: 18px; height: 18px; + border-radius: 50%; background: var(--accent); cursor: pointer; + box-shadow: 0 0 8px var(--accent-glow); +} +.hidden { display: none !important; } +/* ─── API Usage Section ───────────────────────────────── */ +.api-usage { + margin-top: 24px; + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +.api-usage-toggle { + width: 100%; + display: flex; align-items: center; justify-content: space-between; + padding: 14px 18px; + background: var(--bg-card); + border: none; color: var(--text-secondary); + cursor: pointer; font-family: var(--font-sans); + font-size: 0.82rem; font-weight: 600; + transition: var(--transition); +} +.api-usage-toggle:hover { color: var(--text-primary); background: var(--bg-input); } +.api-usage-toggle i { transition: transform 0.25s ease; } +.api-usage-toggle.open i { transform: rotate(180deg); } +.api-usage-toggle .badge { + background: var(--accent-glow); color: var(--accent); + padding: 2px 8px; border-radius: 10px; font-size: 0.68rem; + font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; + margin-left: 8px; +} +.api-usage-body { + display: none; padding: 18px; + background: var(--bg-primary); + border-top: 1px solid var(--border); +} +.api-usage-body.open { display: block; animation: fadeIn 0.25s ease; } +.api-usage-body .api-endpoint { + margin-bottom: 18px; +} +.api-usage-body .api-endpoint:last-child { margin-bottom: 0; } +.api-method { + display: inline-block; + padding: 2px 8px; border-radius: 4px; + font-size: 0.7rem; font-weight: 700; + font-family: var(--font-mono); + margin-right: 6px; +} +.api-method.post { background: #22c55e20; color: var(--green); } +.api-method.get { background: #06b6d420; color: var(--cyan); } +.api-path { + font-family: var(--font-mono); font-size: 0.82rem; + color: var(--text-primary); +} +.api-desc { + font-size: 0.78rem; color: var(--text-muted); + margin: 4px 0 8px; line-height: 1.4; +} +.api-code { + background: #0d0d14; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 14px 16px; + font-family: var(--font-mono); + font-size: 0.76rem; + color: var(--text-secondary); + line-height: 1.65; + overflow-x: auto; + white-space: pre; + position: relative; +} +.api-code .kw { color: var(--pink); } +.api-code .str { color: var(--green); } +.api-code .cm { color: var(--text-muted); font-style: italic; } +.api-code .fn { color: var(--cyan); } +.api-code .var { color: var(--orange); } +.api-code-copy { + position: absolute; top: 8px; right: 8px; + background: var(--bg-input); border: 1px solid var(--border); + color: var(--text-muted); border-radius: 4px; + padding: 3px 8px; font-size: 0.68rem; cursor: pointer; + transition: var(--transition); font-family: var(--font-sans); +} +.api-code-copy:hover { color: var(--accent); border-color: var(--accent); } +.api-baseurl-note { + background: var(--bg-input); border: 1px solid var(--border); + border-radius: var(--radius-sm); padding: 10px 14px; + font-size: 0.76rem; color: var(--text-muted); + margin-bottom: 14px; line-height: 1.5; +} +.api-baseurl-note code { + background: #0d0d14; padding: 1px 6px; border-radius: 3px; + font-family: var(--font-mono); color: var(--accent); font-size: 0.74rem; +} + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..d753f7c --- /dev/null +++ b/public/index.html @@ -0,0 +1,429 @@ + + + + + + ⚡ WinnieAPI-v2 — Developer Utilities + + + + + +
+
+
Copied to clipboard! ✓
+ + +
+
+ + +
+ +
+
+
+ +
+ +
+
+

Your Developer Toolbox

+

Fast, free, privacy-friendly utilities — right in your browser. No sign-up, no tracking, no nonsense.

+
+
0
Tools
+
0
Ads
+
Usage
+
+
+ +
+ + + + + + + +
+ +
+ +
+
+
+

JSON Formatter

+ Format +
+

Beautify, minify, and validate JSON with configurable indentation.

+
+ +
+
+
+

URL Shortener

+ Web +
+

Create short, shareable links instantly with click tracking.

+
+ +
+
+
+

YouTube Tool

+ Video +
+

Extract thumbnails, metadata, and embed codes from YouTube videos.

+
+ +
+
+
+

Base64 Encoder

+ Encode +
+

Encode and decode Base64 strings instantly.

+
+ +
+
+
+

Hash Generator

+ Crypto +
+

Generate MD5, SHA-1, SHA-256, and SHA-512 hashes from text.

+
+ +
+
+
+

Color Converter

+ Design +
+

Convert colors between HEX, RGB, and HSL with live preview.

+
+ +
+
+
+

Password Generator

+ Security +
+

Generate strong, cryptographically secure passwords with custom rules.

+
+ +
+
+
+

UUID Generator

+ Generate +
+

Generate random v4 UUIDs. Batch generate up to 100 at once.

+
+ +
+
+
+

Lorem Ipsum

+ Text +
+

Generate placeholder text with configurable paragraph count.

+
+ +
+
+
+

JWT Decoder

+ Auth +
+

Decode JWT tokens and inspect header, payload, and expiry status.

+
+ +
+
+
+

Timestamp Converter

+ Time +
+

Convert between Unix timestamps, ISO dates, and human-readable formats.

+
+ +
+
+
+

Regex Tester

+ Dev +
+

Test regular expressions with live matching and capture groups.

+
+ +
+
+
+

Markdown Preview

+ Format +
+

Live preview Markdown as you type with full formatting support.

+
+ +
+
+
+

Diff Checker

+ Compare +
+

Compare two texts side-by-side and find differences line by line.

+
+ +
+
+
+

Word Counter

+ Text +
+

Count words, characters, sentences, and get reading time estimates.

+
+ +
+
+
+

CSS Minifier

+ Format +
+

Minify CSS by removing comments, whitespace, and unnecessary characters.

+
+ +
+
+
+

HTML Entities

+ Encode +
+

Encode and decode HTML entities for safe embedding.

+
+ +
+
+
+

Case Converter

+ Text +
+

Convert text between camelCase, snake_case, UPPER, Title Case, and more.

+
+ +
+
+
+

Number Base

+ Math +
+

Convert numbers between decimal, binary, octal, and hexadecimal.

+
+ +
+
+
+

IP Lookup

+ Network +
+

Get geolocation, ISP, and timezone info for any IP address.

+
+ +
+
+
+

QR Code Generator

+ Generate +
+

Generate QR codes from text, URLs, or any data. Download as PNG.

+
+ +
+
+
+

QR Code Reader

+ Scanner +
+

Scan QR codes from your camera or an uploaded image file.

+
+ +
+
+
+

String Escape

+ Encode +
+

Escape and unescape strings (backslash sequences) for programming.

+
+ +
+
+
+

Cron Parser

+ Scheduler +
+

Parse cron expressions into human-readable descriptions with next run times.

+
+ +
+
+
+

Placeholder Image

+ Design +
+

Generate custom placeholder images with text, colors, and dimensions.

+
+ +
+
+
+

JSON ↔ CSV

+ Convert +
+

Convert between JSON arrays and CSV format with download support.

+
+ +
+
+
+

Text Encoder

+ Encode +
+

Encode text in ROT13, Binary, Morse Code, L33t speak, and more.

+
+ +
+
+
+

HTTP Status Codes

+ Reference +
+

Quick reference for all HTTP status codes with descriptions and categories.

+
+ +
+
+
+

SQL Formatter

+ Format +
+

Beautify and minify SQL queries with keyword formatting.

+
+ +
+
+
+

Byte Converter

+ Math +
+

Convert between bytes, KB, MB, GB, TB with binary and SI modes.

+
+ +
+
+
+

HMAC Generator

+ Security +
+

Generate HMAC signatures from a message and secret key.

+
+ +
+
+
+

Slug Generator

+ Text +
+

Convert any text into a clean, URL-friendly slug.

+
+ +
+
+
+

Chmod Calculator

+ Linux +
+

Convert between numeric (755) and symbolic (rwxr-xr-x) Unix permissions.

+
+ +
+
+
+

ASCII Art

+ Generate +
+

Convert text into ASCII art banners using block characters.

+
+ +
+
+
+

ENV ↔ JSON

+ Convert +
+

Convert between .env files and JSON objects instantly.

+
+
+
+ + +
+
+ + + + + + + + + + + + + + + + diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..081a486 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,88 @@ +// ═══════════════════════════════════════════════════════ +// Particles +// ═══════════════════════════════════════════════════════ +(function initParticles() { + const c = document.getElementById('particles'); + for (let i = 0; i < 30; i++) { + const p = document.createElement('div'); + p.className = 'particle'; + p.style.left = Math.random() * 100 + '%'; + p.style.animationDelay = Math.random() * 15 + 's'; + p.style.animationDuration = (10 + Math.random() * 20) + 's'; + p.style.width = p.style.height = (1 + Math.random() * 3) + 'px'; + c.appendChild(p); + } +})(); + +// ═══════════════════════════════════════════════════════ +// Navigation +// ═══════════════════════════════════════════════════════ +function showPage(name) { + document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); + document.getElementById('page-' + name).classList.add('active'); + window.scrollTo({ top: 0, behavior: 'smooth' }); +} + +// ═══════════════════════════════════════════════════════ +// Search & Filter +// ═══════════════════════════════════════════════════════ +function filterTools(query) { + const q = query.toLowerCase(); + document.querySelectorAll('.tool-card').forEach(card => { + const name = card.dataset.name.toLowerCase(); + card.style.display = name.includes(q) ? '' : 'none'; + }); +} +function filterCategory(cat, btn) { + document.querySelectorAll('.cat-btn').forEach(b => b.classList.remove('active')); + if (btn) btn.classList.add('active'); + document.querySelectorAll('.tool-card').forEach(card => { + card.style.display = (cat === 'all' || card.dataset.cat === cat) ? '' : 'none'; + }); +} +// Ctrl+K to focus search +document.addEventListener('keydown', e => { + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + const el = document.getElementById('searchInput'); + if (el) el.focus(); + } +}); + +// ═══════════════════════════════════════════════════════ +// Helpers +// ═══════════════════════════════════════════════════════ +// ✦ Change this for production deployment: +const BASE_URL = window.location.origin; // e.g. "https://winnieapi-v2.yourdomain.com" + +function copyText(text) { + navigator.clipboard.writeText(text).then(() => { + const t = document.getElementById('copyToast'); + t.classList.add('show'); + setTimeout(() => t.classList.remove('show'), 1500); + }); +} +function copyOutput(id) { copyText(document.getElementById(id).value); } +function setStatus(id, type, msg) { + const el = document.getElementById(id); + el.className = 'status ' + type; + el.textContent = msg; +} +async function apiPost(url, body) { + const r = await fetch(BASE_URL + url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) }); + return r.json(); +} +async function apiGet(url) { + const r = await fetch(BASE_URL + url); + return r.json(); +} +function toggleApiUsage(btn) { + btn.classList.toggle('open'); + const body = btn.nextElementSibling; + body.classList.toggle('open'); +} +function copyApiCode(btn) { + const code = btn.parentElement.textContent.replace('Copy', '').trim(); + copyText(code); +} + diff --git a/public/js/tools/ascii.js b/public/js/tools/ascii.js new file mode 100644 index 0000000..028ae14 --- /dev/null +++ b/public/js/tools/ascii.js @@ -0,0 +1,14 @@ +// ═══════════════════════════════════════════════════════ +// ASCII Art Generator +// ═══════════════════════════════════════════════════════ +async function generateAscii() { + const text = document.getElementById('asciiInput').value.trim(); + if (!text) { document.getElementById('asciiOutput').value = ''; return; } + const d = await apiPost('/api/ascii/generate', { text }); + if (d.success) { + document.getElementById('asciiOutput').value = d.result; + setStatus('asciiStatus', 'success', 'Generated ✓'); + } else setStatus('asciiStatus', 'error', d.error); +} +setTimeout(generateAscii, 200); + diff --git a/public/js/tools/base64.js b/public/js/tools/base64.js new file mode 100644 index 0000000..93cb43d --- /dev/null +++ b/public/js/tools/base64.js @@ -0,0 +1,11 @@ +// ═══════════════════════════════════════════════════════ +// Base64 +// ═══════════════════════════════════════════════════════ +async function base64Op(op) { + const text = document.getElementById('b64Input').value; + if (!text) return setStatus('b64Status','error','Enter some text.'); + const d = await apiPost('/api/base64/' + op, { text }); + if (d.success) { document.getElementById('b64Output').value = d.result; setStatus('b64Status','success', op === 'encode' ? 'Encoded ✓' : 'Decoded ✓'); } + else setStatus('b64Status','error', d.error); +} + diff --git a/public/js/tools/byteconv.js b/public/js/tools/byteconv.js new file mode 100644 index 0000000..c773fdd --- /dev/null +++ b/public/js/tools/byteconv.js @@ -0,0 +1,22 @@ +// ═══════════════════════════════════════════════════════ +// Byte Size Converter +// ═══════════════════════════════════════════════════════ +async function convertBytes() { + const value = parseFloat(document.getElementById('byteValue').value); + if (isNaN(value)) return; + const unit = document.getElementById('byteUnit').value; + const mode = document.querySelector('input[name="byteMode"]:checked').value; + const d = await apiPost('/api/bytes/convert', { value, unit, mode }); + if (d.success) { + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + document.getElementById('byteResults').innerHTML = units.map(u => + `
+
${u}
+
${d[u]}
+
` + ).join(''); + setStatus('byteStatus', 'success', `Converted (${mode === 'binary' ? '1024' : '1000'} base) ✓`); + } else setStatus('byteStatus', 'error', d.error); +} +setTimeout(convertBytes, 200); + diff --git a/public/js/tools/caseconv.js b/public/js/tools/caseconv.js new file mode 100644 index 0000000..fd431b8 --- /dev/null +++ b/public/js/tools/caseconv.js @@ -0,0 +1,16 @@ +// ═══════════════════════════════════════════════════════ +// Case Converter +// ═══════════════════════════════════════════════════════ +async function convertCase() { + const text = document.getElementById('caseInput').value; + if (!text) return; + const d = await apiPost('/api/text/case', { text }); + if (d.success) { + document.getElementById('caseResults').innerHTML = [ + ['UPPERCASE', d.uppercase], ['lowercase', d.lowercase], ['Title Case', d.titleCase], + ['camelCase', d.camelCase], ['snake_case', d.snakeCase], ['kebab-case', d.kebabCase], + ['dot.case', d.dotCase], ['desreveR', d.reversed] + ].map(([l, v]) => `
${l}
${v}
`).join(''); + } +} + diff --git a/public/js/tools/chmod.js b/public/js/tools/chmod.js new file mode 100644 index 0000000..a35719b --- /dev/null +++ b/public/js/tools/chmod.js @@ -0,0 +1,36 @@ +// ═══════════════════════════════════════════════════════ +// Chmod Calculator +// ═══════════════════════════════════════════════════════ +async function chmodFromNumeric() { + const val = document.getElementById('chmodNumeric').value.trim(); + if (!val || !/^[0-7]{3,4}$/.test(val)) return; + const d = await apiPost('/api/chmod/calculate', { numeric: val }); + if (d.success) updateChmodUI(d); +} +async function chmodFromSymbolic() { + const val = document.getElementById('chmodSymbolic').value.trim(); + if (!val || val.length < 9) return; + const d = await apiPost('/api/chmod/calculate', { symbolic: val }); + if (d.success) updateChmodUI(d); +} +function updateChmodUI(d) { + document.getElementById('chmodNumeric').value = d.numeric; + document.getElementById('chmodSymbolic').value = d.symbolic; + document.getElementById('chmodCommand').textContent = `chmod ${d.numeric} filename`; + const roles = ['owner', 'group', 'others']; + const perms = ['read', 'write', 'execute']; + let html = '
'; + html += '
' + perms.map(p => `
${p}
`).join(''); + for (const role of roles) { + html += `
${role}
`; + for (const perm of perms) { + const on = d[role][perm]; + html += `
${on ? '✓' : '—'}
`; + } + } + html += '
'; + document.getElementById('chmodMatrix').innerHTML = html; + setStatus('chmodStatus', 'success', `${d.numeric} = ${d.symbolic} ✓`); +} +setTimeout(chmodFromNumeric, 200); + diff --git a/public/js/tools/color.js b/public/js/tools/color.js new file mode 100644 index 0000000..b7edf00 --- /dev/null +++ b/public/js/tools/color.js @@ -0,0 +1,21 @@ +// ═══════════════════════════════════════════════════════ +// Color Converter +// ═══════════════════════════════════════════════════════ +async function convertColor() { + const color = document.getElementById('colorInput').value.trim(); + if (!color) return setStatus('colorStatus','error','Enter a color.'); + const d = await apiPost('/api/color/convert', { color }); + if (d.success) { + document.getElementById('colorPreview').style.background = d.hex; + document.getElementById('colorPicker').value = d.hex; + const c = document.getElementById('colorResults'); + c.innerHTML = [ + ['HEX', d.hex], ['RGB', d.rgb], ['HSL', d.hsl] + ].map(([l, v]) => `
${l}
${v}
`).join(''); + setStatus('colorStatus','success','Converted ✓'); + } else setStatus('colorStatus','error', d.error); +} + +// Keyboard shortcut +document.getElementById('colorInput').addEventListener('keydown', e => { if(e.key==='Enter') convertColor(); }); + diff --git a/public/js/tools/counter.js b/public/js/tools/counter.js new file mode 100644 index 0000000..d42ce3f --- /dev/null +++ b/public/js/tools/counter.js @@ -0,0 +1,21 @@ +// ═══════════════════════════════════════════════════════ +// Word Counter +// ═══════════════════════════════════════════════════════ +async function updateCounter() { + const text = document.getElementById('counterInput').value; + const d = await apiPost('/api/text/stats', { text }); + if (d.success) { + document.getElementById('counterResults').innerHTML = ` +
+
Characters
${d.characters}
+
No Spaces
${d.charactersNoSpaces}
+
Words
${d.words}
+
Sentences
${d.sentences}
+
Paragraphs
${d.paragraphs}
+
Lines
${d.lines}
+
Reading Time
${d.readingTime}
+
+ ${d.topChars.length ? '
Top Characters
' + d.topChars.map(([ch,n]) => `${ch} ×${n}`).join('') + '
' : ''}`; + } +} + diff --git a/public/js/tools/cron.js b/public/js/tools/cron.js new file mode 100644 index 0000000..369104d --- /dev/null +++ b/public/js/tools/cron.js @@ -0,0 +1,28 @@ +// ═══════════════════════════════════════════════════════ +// Cron Parser +// ═══════════════════════════════════════════════════════ +function setCronPreset(expr) { + document.getElementById('cronInput').value = expr; + parseCron(); +} + +async function parseCron() { + const expr = document.getElementById('cronInput').value.trim(); + if (!expr) return setStatus('cronStatus', 'error', 'Enter a cron expression.'); + const d = await apiPost('/api/cron/parse', { expression: expr }); + if (d.success) { + document.getElementById('cronResult').style.display = 'block'; + document.getElementById('cronDescription').textContent = d.description; + document.getElementById('cronFields').innerHTML = Object.entries(d.fields).map(([k, v]) => + `
${k}
${v}
` + ).join(''); + document.getElementById('cronNextRuns').innerHTML = d.nextRuns.map((t, i) => + `
Run #${i + 1}
${t}
` + ).join(''); + setStatus('cronStatus', 'success', 'Parsed ✓'); + } else { + document.getElementById('cronResult').style.display = 'none'; + setStatus('cronStatus', 'error', d.error); + } +} + diff --git a/public/js/tools/cssmin.js b/public/js/tools/cssmin.js new file mode 100644 index 0000000..e56c7b3 --- /dev/null +++ b/public/js/tools/cssmin.js @@ -0,0 +1,13 @@ +// ═══════════════════════════════════════════════════════ +// CSS Minifier +// ═══════════════════════════════════════════════════════ +async function minifyCSS() { + const css = document.getElementById('cssInput').value; + if (!css) return setStatus('cssStatus','error','Enter some CSS.'); + const d = await apiPost('/api/css/minify', { css }); + if (d.success) { + document.getElementById('cssOutput').value = d.result; + setStatus('cssStatus','success', `Minified ✓ — saved ${d.saved} chars (${d.percentage}% smaller)`); + } else setStatus('cssStatus','error', d.error); +} + diff --git a/public/js/tools/diff.js b/public/js/tools/diff.js new file mode 100644 index 0000000..4285705 --- /dev/null +++ b/public/js/tools/diff.js @@ -0,0 +1,23 @@ +// ═══════════════════════════════════════════════════════ +// Diff Checker +// ═══════════════════════════════════════════════════════ +function computeDiff() { + const a = document.getElementById('diffA').value.split('\n'); + const b = document.getElementById('diffB').value.split('\n'); + const max = Math.max(a.length, b.length); + let html = ''; + for (let i = 0; i < max; i++) { + const la = a[i] !== undefined ? a[i] : null; + const lb = b[i] !== undefined ? b[i] : null; + const esc = s => s.replace(/[<>&]/g, c => ({'<':'<','>':'>','&':'&'}[c])); + if (la === null) html += `
+ ${esc(lb)}
`; + else if (lb === null) html += `
- ${esc(la)}
`; + else if (la === lb) html += `
${esc(la)}
`; + else { + html += `
- ${esc(la)}
`; + html += `
+ ${esc(lb)}
`; + } + } + document.getElementById('diffOutput').innerHTML = html || 'Texts are identical.'; +} + diff --git a/public/js/tools/envjson.js b/public/js/tools/envjson.js new file mode 100644 index 0000000..cacfba7 --- /dev/null +++ b/public/js/tools/envjson.js @@ -0,0 +1,22 @@ +// ═══════════════════════════════════════════════════════ +// ENV ↔ JSON Converter +// ═══════════════════════════════════════════════════════ +async function envToJson() { + const input = document.getElementById('envInput').value.trim(); + if (!input) return setStatus('envJsonStatus', 'error', 'Paste some .env content first.'); + const d = await apiPost('/api/convert/env-to-json', { env: input }); + if (d.success) { + document.getElementById('envJsonOutput').value = JSON.stringify(d.result, null, 2); + setStatus('envJsonStatus', 'success', `Converted ✓ — ${Object.keys(d.result).length} variables`); + } else setStatus('envJsonStatus', 'error', d.error); +} +async function jsonToEnv() { + const input = document.getElementById('envJsonInput').value.trim(); + if (!input) return setStatus('envJsonStatus', 'error', 'Paste some JSON first.'); + const d = await apiPost('/api/convert/json-to-env', { json: input }); + if (d.success) { + document.getElementById('envJsonOutput').value = d.result; + setStatus('envJsonStatus', 'success', `Converted ✓ — ${d.count} variables`); + } else setStatus('envJsonStatus', 'error', d.error); +} + diff --git a/public/js/tools/escape.js b/public/js/tools/escape.js new file mode 100644 index 0000000..d1d8762 --- /dev/null +++ b/public/js/tools/escape.js @@ -0,0 +1,11 @@ +// ═══════════════════════════════════════════════════════ +// String Escape +// ═══════════════════════════════════════════════════════ +async function escapeOp(op) { + const text = document.getElementById('escInput').value; + if (!text) return setStatus('escStatus','error','Enter text.'); + const d = await apiPost('/api/' + op, { text }); + if (d.success) { document.getElementById('escOutput').value = d.result; setStatus('escStatus','success', op === 'escape' ? 'Escaped ✓' : 'Unescaped ✓'); } + else setStatus('escStatus','error', d.error); +} + diff --git a/public/js/tools/hash.js b/public/js/tools/hash.js new file mode 100644 index 0000000..685c01e --- /dev/null +++ b/public/js/tools/hash.js @@ -0,0 +1,18 @@ +// ═══════════════════════════════════════════════════════ +// Hash +// ═══════════════════════════════════════════════════════ +async function generateHash() { + const text = document.getElementById('hashInput').value; + if (!text) return setStatus('hashStatus','error','Enter text to hash.'); + const d = await apiPost('/api/hash', { text }); + if (d.success) { + const c = document.getElementById('hashResults'); + c.innerHTML = Object.entries(d.hashes).map(([algo, hash]) => ` +
+
${algo.toUpperCase()}
+
${hash}
+
`).join(''); + setStatus('hashStatus','success','Generated ✓'); + } else setStatus('hashStatus','error', d.error); +} + diff --git a/public/js/tools/hmac.js b/public/js/tools/hmac.js new file mode 100644 index 0000000..4a8c2db --- /dev/null +++ b/public/js/tools/hmac.js @@ -0,0 +1,20 @@ +// ═══════════════════════════════════════════════════════ +// HMAC Generator +// ═══════════════════════════════════════════════════════ +async function generateHmac() { + const message = document.getElementById('hmacMessage').value; + const secret = document.getElementById('hmacSecret').value; + const algorithm = document.getElementById('hmacAlgo').value; + if (!message) return setStatus('hmacStatus', 'error', 'Enter a message.'); + if (!secret) return setStatus('hmacStatus', 'error', 'Enter a secret key.'); + const d = await apiPost('/api/hmac', { message, secret, algorithm }); + if (d.success) { + document.getElementById('hmacResults').innerHTML = ` +
+
${d.algorithm.toUpperCase()} HMAC
+
${d.hmac}
+
`; + setStatus('hmacStatus', 'success', 'Generated ✓'); + } else setStatus('hmacStatus', 'error', d.error); +} + diff --git a/public/js/tools/htmlent.js b/public/js/tools/htmlent.js new file mode 100644 index 0000000..13cd9ce --- /dev/null +++ b/public/js/tools/htmlent.js @@ -0,0 +1,11 @@ +// ═══════════════════════════════════════════════════════ +// HTML Entities +// ═══════════════════════════════════════════════════════ +async function htmlEntOp(op) { + const text = document.getElementById('htmlEntInput').value; + if (!text) return setStatus('htmlEntStatus','error','Enter text.'); + const d = await apiPost('/api/html/' + op, { text }); + if (d.success) { document.getElementById('htmlEntOutput').value = d.result; setStatus('htmlEntStatus','success', op === 'encode' ? 'Encoded ✓' : 'Decoded ✓'); } + else setStatus('htmlEntStatus','error', d.error); +} + diff --git a/public/js/tools/httpstatus.js b/public/js/tools/httpstatus.js new file mode 100644 index 0000000..e8f9f00 --- /dev/null +++ b/public/js/tools/httpstatus.js @@ -0,0 +1,103 @@ +// ═══════════════════════════════════════════════════════ +// HTTP Status Codes Reference +// ═══════════════════════════════════════════════════════ +const HTTP_CODES = [ + // 1xx + { code: 100, text: 'Continue', desc: 'The server has received the request headers, and the client should proceed to send the request body.', cat: '1xx' }, + { code: 101, text: 'Switching Protocols', desc: 'The server is switching protocols as requested by the client (e.g., to WebSocket).', cat: '1xx' }, + { code: 102, text: 'Processing', desc: 'The server has received and is processing the request, but no response is available yet.', cat: '1xx' }, + { code: 103, text: 'Early Hints', desc: 'Used to return some response headers before the final HTTP message.', cat: '1xx' }, + // 2xx + { code: 200, text: 'OK', desc: 'The request has succeeded. The meaning depends on the HTTP method used.', cat: '2xx' }, + { code: 201, text: 'Created', desc: 'The request has been fulfilled and a new resource has been created.', cat: '2xx' }, + { code: 202, text: 'Accepted', desc: 'The request has been accepted for processing, but processing is not complete.', cat: '2xx' }, + { code: 203, text: 'Non-Authoritative Information', desc: 'The returned meta-information is from a local or third-party copy.', cat: '2xx' }, + { code: 204, text: 'No Content', desc: 'The server has fulfilled the request but does not need to return an entity-body.', cat: '2xx' }, + { code: 205, text: 'Reset Content', desc: 'The server has fulfilled the request and the user agent should reset the document view.', cat: '2xx' }, + { code: 206, text: 'Partial Content', desc: 'The server is delivering only part of the resource due to a range header sent by the client.', cat: '2xx' }, + { code: 207, text: 'Multi-Status', desc: 'A Multi-Status response conveys information about multiple resources (WebDAV).', cat: '2xx' }, + { code: 208, text: 'Already Reported', desc: 'Members of a DAV binding have already been enumerated in a previous reply.', cat: '2xx' }, + { code: 226, text: 'IM Used', desc: 'The server has fulfilled a GET request for the resource with instance manipulations applied.', cat: '2xx' }, + // 3xx + { code: 300, text: 'Multiple Choices', desc: 'There are multiple options for the resource, each with specific attributes.', cat: '3xx' }, + { code: 301, text: 'Moved Permanently', desc: 'This and all future requests should be directed to the given URI.', cat: '3xx' }, + { code: 302, text: 'Found', desc: 'The resource was found at a different URI temporarily.', cat: '3xx' }, + { code: 303, text: 'See Other', desc: 'The response can be found at another URI using a GET method.', cat: '3xx' }, + { code: 304, text: 'Not Modified', desc: 'The resource has not been modified since the last request.', cat: '3xx' }, + { code: 307, text: 'Temporary Redirect', desc: 'The request should be repeated with another URI, but future requests should still use the original URI.', cat: '3xx' }, + { code: 308, text: 'Permanent Redirect', desc: 'This and all future requests should be directed to the given URI (no method change).', cat: '3xx' }, + // 4xx + { code: 400, text: 'Bad Request', desc: 'The server cannot process the request due to a client error (e.g., malformed request syntax).', cat: '4xx' }, + { code: 401, text: 'Unauthorized', desc: 'Authentication is required and has failed or has not been provided.', cat: '4xx' }, + { code: 402, text: 'Payment Required', desc: 'Reserved for future use. Some APIs use this for rate limiting or paid features.', cat: '4xx' }, + { code: 403, text: 'Forbidden', desc: 'The server understood the request but refuses to authorize it.', cat: '4xx' }, + { code: 404, text: 'Not Found', desc: 'The requested resource could not be found on this server.', cat: '4xx' }, + { code: 405, text: 'Method Not Allowed', desc: 'The request method is not supported for the requested resource.', cat: '4xx' }, + { code: 406, text: 'Not Acceptable', desc: 'The requested resource can only generate content not acceptable per the Accept headers.', cat: '4xx' }, + { code: 407, text: 'Proxy Authentication Required', desc: 'The client must authenticate itself with the proxy.', cat: '4xx' }, + { code: 408, text: 'Request Timeout', desc: 'The server timed out waiting for the request.', cat: '4xx' }, + { code: 409, text: 'Conflict', desc: 'The request could not be processed because of conflict in the current state of the resource.', cat: '4xx' }, + { code: 410, text: 'Gone', desc: 'The resource requested is no longer available and will not be available again.', cat: '4xx' }, + { code: 411, text: 'Length Required', desc: 'The request did not specify the length of its content, which is required.', cat: '4xx' }, + { code: 412, text: 'Precondition Failed', desc: 'The server does not meet one of the preconditions set by the requester.', cat: '4xx' }, + { code: 413, text: 'Payload Too Large', desc: 'The request is larger than the server is willing or able to process.', cat: '4xx' }, + { code: 414, text: 'URI Too Long', desc: 'The URI provided was too long for the server to process.', cat: '4xx' }, + { code: 415, text: 'Unsupported Media Type', desc: 'The request entity has a media type which the server does not support.', cat: '4xx' }, + { code: 416, text: 'Range Not Satisfiable', desc: 'The client has asked for a portion of the file that the server cannot supply.', cat: '4xx' }, + { code: 418, text: "I'm a Teapot", desc: "The server refuses to brew coffee because it is, permanently, a teapot. (RFC 2324)", cat: '4xx' }, + { code: 422, text: 'Unprocessable Entity', desc: 'The request was well-formed but was unable to be followed due to semantic errors.', cat: '4xx' }, + { code: 425, text: 'Too Early', desc: 'The server is unwilling to risk processing a request that might be replayed.', cat: '4xx' }, + { code: 429, text: 'Too Many Requests', desc: 'The user has sent too many requests in a given amount of time (rate limiting).', cat: '4xx' }, + { code: 451, text: 'Unavailable For Legal Reasons', desc: 'The resource is unavailable due to legal demands (e.g., censorship or government order).', cat: '4xx' }, + // 5xx + { code: 500, text: 'Internal Server Error', desc: 'An unexpected condition was encountered by the server.', cat: '5xx' }, + { code: 501, text: 'Not Implemented', desc: 'The server does not support the functionality required to fulfill the request.', cat: '5xx' }, + { code: 502, text: 'Bad Gateway', desc: 'The server received an invalid response from an upstream server.', cat: '5xx' }, + { code: 503, text: 'Service Unavailable', desc: 'The server is currently unable to handle the request (overloaded or maintenance).', cat: '5xx' }, + { code: 504, text: 'Gateway Timeout', desc: 'The server did not receive a timely response from an upstream server.', cat: '5xx' }, + { code: 505, text: 'HTTP Version Not Supported', desc: 'The server does not support the HTTP protocol version used in the request.', cat: '5xx' }, + { code: 507, text: 'Insufficient Storage', desc: 'The server is unable to store the representation needed to complete the request.', cat: '5xx' }, + { code: 508, text: 'Loop Detected', desc: 'The server detected an infinite loop while processing the request.', cat: '5xx' }, + { code: 511, text: 'Network Authentication Required', desc: 'The client needs to authenticate to gain network access (captive portal).', cat: '5xx' }, +]; + +const catColors = { '1xx': 'var(--accent)', '2xx': 'var(--green)', '3xx': 'var(--cyan)', '4xx': 'var(--orange)', '5xx': 'var(--red)' }; +let currentHttpCat = 'all'; + +function renderHttpStatus(filtered) { + const list = document.getElementById('httpStatusList'); + if (filtered.length === 0) { + list.innerHTML = '
No matching status codes found.
'; + return; + } + list.innerHTML = filtered.map(c => ` +
+
+ ${c.code} + ${c.text} + ${c.cat} +
+
${c.desc}
+
+ `).join(''); +} + +function filterHttpStatus() { + const q = document.getElementById('httpStatusInput').value.toLowerCase(); + let filtered = HTTP_CODES; + if (currentHttpCat !== 'all') filtered = filtered.filter(c => c.cat === currentHttpCat); + if (q) filtered = filtered.filter(c => String(c.code).includes(q) || c.text.toLowerCase().includes(q) || c.desc.toLowerCase().includes(q)); + renderHttpStatus(filtered); +} + +function filterHttpCat(cat, btn) { + currentHttpCat = cat; + document.querySelectorAll('#httpCatBtns .btn').forEach(b => { + b.className = b === btn ? 'btn btn-sm btn-primary' : 'btn btn-sm btn-secondary'; + }); + filterHttpStatus(); +} + +// Initialize +setTimeout(() => renderHttpStatus(HTTP_CODES), 100); + diff --git a/public/js/tools/ip.js b/public/js/tools/ip.js new file mode 100644 index 0000000..289f3be --- /dev/null +++ b/public/js/tools/ip.js @@ -0,0 +1,23 @@ +// ═══════════════════════════════════════════════════════ +// IP Lookup +// ═══════════════════════════════════════════════════════ +async function lookupIP() { + const ip = document.getElementById('ipInput').value.trim(); + setStatus('ipStatus','info','Looking up...'); + const d = await apiGet(ip ? '/api/ip/' + ip : '/api/ip'); + if (d.success && d.status !== 'fail') { + const fields = [ + ['IP Address', d.query], ['Country', d.country], ['Region', d.regionName], + ['City', d.city], ['ZIP', d.zip], ['Latitude', d.lat], + ['Longitude', d.lon], ['Timezone', d.timezone], ['ISP', d.isp], + ['Organization', d.org], ['AS', d.as] + ]; + document.getElementById('ipResults').innerHTML = fields.map(([l, v]) => + `
${l}
${v || '—'}
`).join(''); + setStatus('ipStatus','success','Lookup complete ✓'); + } else setStatus('ipStatus','error', d.message || d.error || 'Lookup failed'); +} + +// Keyboard shortcut +document.getElementById('ipInput').addEventListener('keydown', e => { if(e.key==='Enter') lookupIP(); }); + diff --git a/public/js/tools/json.js b/public/js/tools/json.js new file mode 100644 index 0000000..1e8c193 --- /dev/null +++ b/public/js/tools/json.js @@ -0,0 +1,36 @@ +// ═══════════════════════════════════════════════════════ +// JSON Formatter +// ═══════════════════════════════════════════════════════ +async function formatJSON() { + const input = document.getElementById('jsonInput').value.trim(); + if (!input) return setStatus('jsonStatus','error','Paste some JSON first.'); + const iv = document.getElementById('jsonIndent').value; + const indent = iv === '\\t' ? '\t' : parseInt(iv); + const d = await apiPost('/api/json/format', { json: input, indent }); + if (d.success) { document.getElementById('jsonOutput').value = d.result; setStatus('jsonStatus','success','Formatted ✓'); } + else setStatus('jsonStatus','error', d.error); +} +async function minifyJSON() { + const input = document.getElementById('jsonInput').value.trim(); + if (!input) return setStatus('jsonStatus','error','Paste some JSON first.'); + const d = await apiPost('/api/json/minify', { json: input }); + if (d.success) { document.getElementById('jsonOutput').value = d.result; setStatus('jsonStatus','success','Minified ✓'); } + else setStatus('jsonStatus','error', d.error); +} +async function validateJSON() { + const input = document.getElementById('jsonInput').value.trim(); + if (!input) return setStatus('jsonStatus','error','Paste some JSON first.'); + const d = await apiPost('/api/json/validate', { json: input }); + setStatus('jsonStatus', d.valid ? 'success' : 'error', d.message); +} +function clearJSON() { + document.getElementById('jsonInput').value=''; document.getElementById('jsonOutput').value=''; + document.getElementById('jsonStatus').className='status'; +} + +// Keyboard shortcuts +document.getElementById('jsonInput').addEventListener('keydown', e => { + if(e.key==='Enter' && (e.ctrlKey||e.metaKey)) formatJSON(); + if(e.key==='Tab') { e.preventDefault(); const t=e.target,s=t.selectionStart,en=t.selectionEnd; t.value=t.value.substring(0,s)+' '+t.value.substring(en); t.selectionStart=t.selectionEnd=s+2; } +}); + diff --git a/public/js/tools/jsoncsv.js b/public/js/tools/jsoncsv.js new file mode 100644 index 0000000..ddf7d9f --- /dev/null +++ b/public/js/tools/jsoncsv.js @@ -0,0 +1,41 @@ +// ═══════════════════════════════════════════════════════ +// JSON ↔ CSV Converter +// ═══════════════════════════════════════════════════════ +let lastJsoncsvType = 'csv'; // track last conversion type for download + +async function jsonToCsv() { + const input = document.getElementById('jsoncsvJsonInput').value.trim(); + if (!input) return setStatus('jsoncsvStatus', 'error', 'Paste some JSON first.'); + const d = await apiPost('/api/convert/json-to-csv', { json: input }); + if (d.success) { + document.getElementById('jsoncsvOutput').value = d.result; + lastJsoncsvType = 'csv'; + setStatus('jsoncsvStatus', 'success', `Converted ✓ — ${d.rows} rows, ${d.columns} columns`); + } else setStatus('jsoncsvStatus', 'error', d.error); +} + +async function csvToJson() { + const input = document.getElementById('jsoncsvCsvInput').value.trim(); + if (!input) return setStatus('jsoncsvStatus', 'error', 'Paste some CSV first.'); + const d = await apiPost('/api/convert/csv-to-json', { csv: input }); + if (d.success) { + document.getElementById('jsoncsvOutput').value = JSON.stringify(d.result, null, 2); + lastJsoncsvType = 'json'; + setStatus('jsoncsvStatus', 'success', `Converted ✓ — ${d.result.length} records`); + } else setStatus('jsoncsvStatus', 'error', d.error); +} + +function downloadJsoncsvOutput() { + const output = document.getElementById('jsoncsvOutput').value; + if (!output) return setStatus('jsoncsvStatus', 'error', 'Nothing to download.'); + const ext = lastJsoncsvType === 'csv' ? 'csv' : 'json'; + const mime = lastJsoncsvType === 'csv' ? 'text/csv' : 'application/json'; + const blob = new Blob([output], { type: mime }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = `converted.${ext}`; + a.click(); + URL.revokeObjectURL(a.href); + setStatus('jsoncsvStatus', 'success', 'Downloaded ✓'); +} + diff --git a/public/js/tools/jwt.js b/public/js/tools/jwt.js new file mode 100644 index 0000000..2b7c8d4 --- /dev/null +++ b/public/js/tools/jwt.js @@ -0,0 +1,22 @@ +// ═══════════════════════════════════════════════════════ +// JWT Decoder +// ═══════════════════════════════════════════════════════ +async function decodeJWT() { + const token = document.getElementById('jwtInput').value.trim(); + if (!token) return setStatus('jwtStatus','error','Paste a JWT.'); + const d = await apiPost('/api/jwt/decode', { token }); + if (d.success) { + const expStr = d.expired === null ? '—' : d.expired ? 'EXPIRED ✗' : 'VALID ✓'; + document.getElementById('jwtResults').innerHTML = ` +
+
Header
+
${JSON.stringify(d.header, null, 2)}
+
Payload
+
${JSON.stringify(d.payload, null, 2)}
+
Expiry
+
${expStr}
+
`; + setStatus('jwtStatus','success','Decoded ✓'); + } else setStatus('jwtStatus','error', d.error); +} + diff --git a/public/js/tools/lorem.js b/public/js/tools/lorem.js new file mode 100644 index 0000000..a4aef05 --- /dev/null +++ b/public/js/tools/lorem.js @@ -0,0 +1,8 @@ +// ═══════════════════════════════════════════════════════ +// Lorem Ipsum +// ═══════════════════════════════════════════════════════ +async function generateLorem() { + const d = await apiPost('/api/lorem', { paragraphs: parseInt(document.getElementById('loremCount').value) }); + if (d.success) document.getElementById('loremOutput').value = d.result; +} + diff --git a/public/js/tools/markdown.js b/public/js/tools/markdown.js new file mode 100644 index 0000000..b5d2867 --- /dev/null +++ b/public/js/tools/markdown.js @@ -0,0 +1,30 @@ +// ═══════════════════════════════════════════════════════ +// Markdown Preview +// ═══════════════════════════════════════════════════════ +function renderMarkdown() { + const md = document.getElementById('mdInput').value; + if (!md) { document.getElementById('mdPreview').innerHTML = 'Preview will appear here...'; return; } + let html = md + .replace(/^### (.+)$/gm, '

$1

') + .replace(/^## (.+)$/gm, '

$1

') + .replace(/^# (.+)$/gm, '

$1

') + .replace(/^---$/gm, '
') + .replace(/```([\s\S]*?)```/g, '
$1
') + .replace(/`([^`]+)`/g, '$1') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/~~(.+?)~~/g, '$1') + .replace(/^\> (.+)$/gm, '
$1
') + .replace(/^\- (.+)$/gm, '
  • $1
  • ') + .replace(/^\* (.+)$/gm, '
  • $1
  • ') + .replace(/^\d+\. (.+)$/gm, '
  • $1
  • ') + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') + .replace(/\n\n/g, '

    ') + .replace(/\n/g, '
    '); + html = '

    ' + html + '

    '; + // Wrap consecutive li in ul + html = html.replace(/(
  • .*?<\/li>)+/gs, ''); + document.getElementById('mdPreview').innerHTML = html; +} + diff --git a/public/js/tools/numbase.js b/public/js/tools/numbase.js new file mode 100644 index 0000000..e28193c --- /dev/null +++ b/public/js/tools/numbase.js @@ -0,0 +1,18 @@ +// ═══════════════════════════════════════════════════════ +// Number Base Converter +// ═══════════════════════════════════════════════════════ +async function convertNumber() { + const v = document.getElementById('numInput').value.trim(); + if (!v) return setStatus('numStatus','error','Enter a number.'); + const d = await apiPost('/api/number/convert', { value: v, fromBase: document.getElementById('numBase').value }); + if (d.success) { + document.getElementById('numResults').innerHTML = [ + ['Decimal', d.decimal], ['Binary', d.binary], ['Octal', d.octal], ['Hexadecimal', d.hex] + ].map(([l, v]) => `
    ${l}
    ${v}
    `).join(''); + setStatus('numStatus','success','Converted ✓'); + } else setStatus('numStatus','error', d.error); +} + +// Keyboard shortcut +document.getElementById('numInput').addEventListener('keydown', e => { if(e.key==='Enter') convertNumber(); }); + diff --git a/public/js/tools/password.js b/public/js/tools/password.js new file mode 100644 index 0000000..2c675d2 --- /dev/null +++ b/public/js/tools/password.js @@ -0,0 +1,27 @@ +// ═══════════════════════════════════════════════════════ +// Password Generator +// ═══════════════════════════════════════════════════════ +async function generatePassword() { + const d = await apiPost('/api/password', { + length: parseInt(document.getElementById('pwLength').value), + uppercase: document.getElementById('pwUpper').checked, + lowercase: document.getElementById('pwLower').checked, + numbers: document.getElementById('pwNumbers').checked, + symbols: document.getElementById('pwSymbols').checked, + count: 5 + }); + if (d.success) { + document.getElementById('pwDisplay').textContent = d.passwords[0]; + // Strength + const len = d.passwords[0].length; + const str = len >= 20 ? 100 : len >= 12 ? 75 : len >= 8 ? 50 : 25; + const colors = { 25: 'var(--red)', 50: 'var(--orange)', 75: 'var(--yellow)', 100: 'var(--green)' }; + const fill = document.getElementById('pwStrength'); + fill.style.width = str + '%'; + fill.style.background = colors[str]; + // Batch + document.getElementById('pwBatch').innerHTML = '
    More Passwords
    ' + + d.passwords.slice(1).map(pw => `
    ${pw}
    `).join(''); + } +} + diff --git a/public/js/tools/placeholder.js b/public/js/tools/placeholder.js new file mode 100644 index 0000000..b47b3d0 --- /dev/null +++ b/public/js/tools/placeholder.js @@ -0,0 +1,60 @@ +// ═══════════════════════════════════════════════════════ +// Placeholder Image Generator +// ═══════════════════════════════════════════════════════ +function generatePlaceholder() { + const w = parseInt(document.getElementById('phWidth').value) || 400; + const h = parseInt(document.getElementById('phHeight').value) || 300; + const text = document.getElementById('phText').value || `${w} × ${h}`; + const bg = document.getElementById('phBg').value; + const fg = document.getElementById('phFg').value; + const fontSize = parseInt(document.getElementById('phFontSize').value) || 28; + + document.getElementById('phBgText').value = bg; + document.getElementById('phFgText').value = fg; + document.getElementById('phFontSizeVal').textContent = fontSize + 'px'; + + const canvas = document.getElementById('phCanvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + + // Background + ctx.fillStyle = bg; + ctx.fillRect(0, 0, w, h); + + // Cross lines + ctx.strokeStyle = fg + '20'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(0, 0); ctx.lineTo(w, h); + ctx.moveTo(w, 0); ctx.lineTo(0, h); + ctx.stroke(); + + // Text + ctx.fillStyle = fg; + ctx.font = `${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(text, w / 2, h / 2); +} + +function downloadPlaceholder() { + const canvas = document.getElementById('phCanvas'); + const link = document.createElement('a'); + const w = document.getElementById('phWidth').value || 400; + const h = document.getElementById('phHeight').value || 300; + link.download = `placeholder-${w}x${h}.png`; + link.href = canvas.toDataURL('image/png'); + link.click(); + setStatus('phStatus', 'success', 'Downloaded ✓'); +} + +function copyPlaceholderDataUrl() { + const canvas = document.getElementById('phCanvas'); + copyText(canvas.toDataURL('image/png')); + setStatus('phStatus', 'success', 'Data URL copied ✓'); +} + +// Initialize on load +setTimeout(generatePlaceholder, 100); + diff --git a/public/js/tools/qrcode.js b/public/js/tools/qrcode.js new file mode 100644 index 0000000..41a2e3c --- /dev/null +++ b/public/js/tools/qrcode.js @@ -0,0 +1,56 @@ +// ═══════════════════════════════════════════════════════ +// QR Code (using qrcode-generator library) +// ═══════════════════════════════════════════════════════ +function generateQR() { + const data = document.getElementById('qrInput').value.trim(); + const container = document.getElementById('qrCanvas'); + const ph = document.getElementById('qrPlaceholder'); + if (!data) { container.style.display = 'none'; ph.style.display = ''; return; } + if (typeof qrcode === 'undefined') { ph.textContent = 'Loading QR library...'; return; } + + try { + const qr = qrcode(0, 'M'); + qr.addData(data); + qr.make(); + container.innerHTML = qr.createSvgTag({ cellSize: 4, margin: 8, scalable: true }); + // Style the SVG + const svg = container.querySelector('svg'); + if (svg) { + svg.style.width = '256px'; + svg.style.height = '256px'; + svg.style.background = '#fff'; + svg.style.borderRadius = '12px'; + } + container.style.display = ''; + ph.style.display = 'none'; + } catch (err) { + ph.textContent = err.message; + container.style.display = 'none'; + ph.style.display = ''; + } +} + +function downloadQR() { + const container = document.getElementById('qrCanvas'); + if (container.style.display === 'none') return; + const svg = container.querySelector('svg'); + if (!svg) return; + + // Convert SVG to PNG via canvas + const svgData = new XMLSerializer().serializeToString(svg); + const img = new Image(); + img.onload = function () { + const canvas = document.createElement('canvas'); + canvas.width = 512; + canvas.height = 512; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, 512, 512); + ctx.drawImage(img, 0, 0, 512, 512); + const a = document.createElement('a'); + a.href = canvas.toDataURL('image/png'); + a.download = 'qrcode.png'; + a.click(); + }; + img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))); +} diff --git a/public/js/tools/qrreader.js b/public/js/tools/qrreader.js new file mode 100644 index 0000000..c5a27af --- /dev/null +++ b/public/js/tools/qrreader.js @@ -0,0 +1,163 @@ +// ═══════════════════════════════════════════════════════ +// QR Code Reader (using jsQR library) +// ═══════════════════════════════════════════════════════ +let qrCameraStream = null; +let qrScanInterval = null; +let qrScanHistoryList = []; + +function startQrCamera() { + if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { + return setStatus('qrReaderStatus', 'error', 'Camera API not supported in this browser.'); + } + + navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }) + .then(stream => { + qrCameraStream = stream; + const video = document.getElementById('qrVideo'); + video.srcObject = stream; + video.play(); + + document.getElementById('qrCameraContainer').style.display = 'block'; + document.getElementById('qrCameraBtn').style.display = 'none'; + document.getElementById('qrStopBtn').style.display = ''; + document.getElementById('qrImagePreview').style.display = 'none'; + + setStatus('qrReaderStatus', 'info', 'Camera active — scanning for QR codes...'); + + // Start scanning frames + const canvas = document.getElementById('qrScanCanvas'); + const ctx = canvas.getContext('2d', { willReadFrequently: true }); + + qrScanInterval = setInterval(() => { + if (video.readyState === video.HAVE_ENOUGH_DATA) { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + if (typeof jsQR !== 'undefined') { + const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'dontInvert' }); + if (code) { + showQrResult(code.data); + setStatus('qrReaderStatus', 'success', 'QR code detected ✓'); + } + } + } + }, 250); + }) + .catch(err => { + setStatus('qrReaderStatus', 'error', 'Camera access denied: ' + err.message); + }); +} + +function stopQrCamera() { + if (qrScanInterval) { clearInterval(qrScanInterval); qrScanInterval = null; } + if (qrCameraStream) { + qrCameraStream.getTracks().forEach(t => t.stop()); + qrCameraStream = null; + } + const video = document.getElementById('qrVideo'); + video.srcObject = null; + + document.getElementById('qrCameraContainer').style.display = 'none'; + document.getElementById('qrCameraBtn').style.display = ''; + document.getElementById('qrStopBtn').style.display = 'none'; + setStatus('qrReaderStatus', 'info', 'Camera stopped.'); +} + +function scanQrFromFile(event) { + const file = event.target.files[0]; + if (!file) return; + + // Stop camera if running + stopQrCamera(); + + const img = document.getElementById('qrPreviewImg'); + const reader = new FileReader(); + reader.onload = function (e) { + img.src = e.target.result; + document.getElementById('qrImagePreview').style.display = 'block'; + + const tempImg = new Image(); + tempImg.onload = function () { + const canvas = document.getElementById('qrImgCanvas'); + const ctx = canvas.getContext('2d', { willReadFrequently: true }); + canvas.width = tempImg.width; + canvas.height = tempImg.height; + ctx.drawImage(tempImg, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + if (typeof jsQR !== 'undefined') { + const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'attemptBoth' }); + if (code) { + showQrResult(code.data); + setStatus('qrReaderStatus', 'success', 'QR code detected in image ✓'); + } else { + setStatus('qrReaderStatus', 'error', 'No QR code found in this image.'); + } + } else { + setStatus('qrReaderStatus', 'error', 'QR scanning library not loaded.'); + } + }; + tempImg.src = e.target.result; + }; + reader.readAsDataURL(file); + + // Reset file input so same file can be re-selected + event.target.value = ''; +} + +function showQrResult(data) { + document.getElementById('qrReaderPlaceholder').style.display = 'none'; + document.getElementById('qrReaderResult').style.display = 'block'; + document.getElementById('qrDecodedText').textContent = data; + + // Detect type + let type = 'Plain Text'; + const openBtn = document.getElementById('qrOpenLinkBtn'); + openBtn.style.display = 'none'; + + if (/^https?:\/\//i.test(data)) { + type = '🔗 URL'; + openBtn.style.display = ''; + } else if (/^mailto:/i.test(data)) { + type = '📧 Email'; + openBtn.style.display = ''; + } else if (/^tel:/i.test(data)) { + type = '📞 Phone Number'; + } else if (/^BEGIN:VCARD/i.test(data)) { + type = '👤 vCard Contact'; + } else if (/^BEGIN:VEVENT/i.test(data)) { + type = '📅 Calendar Event'; + } else if (/^WIFI:/i.test(data)) { + type = '📶 Wi-Fi Network'; + } else if (/^smsto:/i.test(data)) { + type = '💬 SMS'; + } else if (/^geo:/i.test(data)) { + type = '📍 Geolocation'; + } + + document.getElementById('qrDecodedType').textContent = type; + + // Add to history (keep last 10) + const now = new Date().toLocaleTimeString(); + qrScanHistoryList.unshift({ data: data.length > 80 ? data.slice(0, 80) + '...' : data, time: now, full: data }); + if (qrScanHistoryList.length > 10) qrScanHistoryList.pop(); + + document.getElementById('qrScanHistory').innerHTML = qrScanHistoryList.map(h => + `
    +
    ${h.time}
    +
    ${h.data.replace(/ +
    ` + ).join(''); +} + +// Stop camera when navigating away +const origShowPage = window.showPage; +if (origShowPage) { + window.showPage = function (name) { + if (name !== 'qrreader' && qrCameraStream) stopQrCamera(); + origShowPage(name); + }; +} + diff --git a/public/js/tools/regex.js b/public/js/tools/regex.js new file mode 100644 index 0000000..23d4799 --- /dev/null +++ b/public/js/tools/regex.js @@ -0,0 +1,29 @@ +// ═══════════════════════════════════════════════════════ +// Regex Tester +// ═══════════════════════════════════════════════════════ +function testRegex() { + const pattern = document.getElementById('regexPattern').value; + const flags = document.getElementById('regexFlags').value; + const input = document.getElementById('regexInput').value; + const resEl = document.getElementById('regexResults'); + const listEl = document.getElementById('regexMatchList'); + if (!pattern || !input) { resEl.innerHTML = 'Enter a pattern and test string...'; listEl.innerHTML=''; return; } + try { + const re = new RegExp(pattern, flags); + // Highlight matches + let highlighted = input.replace(/[<>&]/g, c => ({'<':'<','>':'>','&':'&'}[c])); + const safePattern = new RegExp(pattern, flags); + highlighted = input.replace(safePattern, m => `${m.replace(/[<>&]/g, c => ({'<':'<','>':'>','&':'&'}[c]))}`); + resEl.innerHTML = highlighted || 'No matches'; + // Match list + const matches = [...input.matchAll(new RegExp(pattern, flags.includes('g') ? flags : flags + 'g'))]; + if (matches.length) { + listEl.innerHTML = '
    Matches (' + matches.length + ')
    ' + + matches.map((m, i) => `
    Match ${i+1}
    ${m[0]}
    `).join(''); + } else listEl.innerHTML = ''; + } catch (e) { + resEl.innerHTML = `${e.message}`; + listEl.innerHTML = ''; + } +} + diff --git a/public/js/tools/slugify.js b/public/js/tools/slugify.js new file mode 100644 index 0000000..93a8850 --- /dev/null +++ b/public/js/tools/slugify.js @@ -0,0 +1,15 @@ +// ═══════════════════════════════════════════════════════ +// Slug Generator +// ═══════════════════════════════════════════════════════ +async function generateSlug() { + const text = document.getElementById('slugInput').value; + if (!text.trim()) { document.getElementById('slugOutput').textContent = '—'; return; } + const separator = document.getElementById('slugSeparator').value; + const lowercase = document.getElementById('slugLower').checked; + const d = await apiPost('/api/text/slugify', { text, separator, lowercase }); + if (d.success) { + document.getElementById('slugOutput').textContent = d.result; + setStatus('slugStatus', 'success', 'Generated ✓'); + } else setStatus('slugStatus', 'error', d.error); +} + diff --git a/public/js/tools/sqlformat.js b/public/js/tools/sqlformat.js new file mode 100644 index 0000000..95793a7 --- /dev/null +++ b/public/js/tools/sqlformat.js @@ -0,0 +1,18 @@ +// ═══════════════════════════════════════════════════════ +// SQL Formatter +// ═══════════════════════════════════════════════════════ +async function formatSQL() { + const input = document.getElementById('sqlInput').value.trim(); + if (!input) return setStatus('sqlStatus', 'error', 'Paste some SQL first.'); + const d = await apiPost('/api/sql/format', { sql: input }); + if (d.success) { document.getElementById('sqlOutput').value = d.result; setStatus('sqlStatus', 'success', 'Formatted ✓'); } + else setStatus('sqlStatus', 'error', d.error); +} +async function minifySQL() { + const input = document.getElementById('sqlInput').value.trim(); + if (!input) return setStatus('sqlStatus', 'error', 'Paste some SQL first.'); + const d = await apiPost('/api/sql/minify', { sql: input }); + if (d.success) { document.getElementById('sqlOutput').value = d.result; setStatus('sqlStatus', 'success', 'Minified ✓'); } + else setStatus('sqlStatus', 'error', d.error); +} + diff --git a/public/js/tools/textenc.js b/public/js/tools/textenc.js new file mode 100644 index 0000000..0bc614e --- /dev/null +++ b/public/js/tools/textenc.js @@ -0,0 +1,21 @@ +// ═══════════════════════════════════════════════════════ +// Text Encoder / Decoder +// ═══════════════════════════════════════════════════════ +async function encodeText(method) { + const text = document.getElementById('textencInput').value; + if (!text) return setStatus('textencStatus', 'error', 'Enter some text first.'); + const d = await apiPost('/api/text/encode', { text, method }); + if (d.success) { + document.getElementById('textencOutput').value = d.result; + setStatus('textencStatus', 'success', `Encoded with ${method} ✓`); + } else setStatus('textencStatus', 'error', d.error); +} + +function swapTextEnc() { + const input = document.getElementById('textencInput'); + const output = document.getElementById('textencOutput'); + const tmp = input.value; + input.value = output.value; + output.value = tmp; +} + diff --git a/public/js/tools/timestamp.js b/public/js/tools/timestamp.js new file mode 100644 index 0000000..c9e7a09 --- /dev/null +++ b/public/js/tools/timestamp.js @@ -0,0 +1,18 @@ +// ═══════════════════════════════════════════════════════ +// Timestamp +// ═══════════════════════════════════════════════════════ +async function convertTimestamp() { + const v = document.getElementById('tsInput').value.trim(); + const d = await apiPost('/api/timestamp', { value: v || 'now' }); + if (d.success) { + document.getElementById('tsResults').innerHTML = [ + ['Unix (s)', d.unix], ['Unix (ms)', d.unixMs], ['ISO 8601', d.iso], + ['UTC', d.utc], ['Local', d.local], ['Relative', d.relative] + ].map(([l, v]) => `
    ${l}
    ${v}
    `).join(''); + setStatus('tsStatus','success','Converted ✓'); + } else setStatus('tsStatus','error', d.error); +} + +// Keyboard shortcut +document.getElementById('tsInput').addEventListener('keydown', e => { if(e.key==='Enter') convertTimestamp(); }); + diff --git a/public/js/tools/url.js b/public/js/tools/url.js new file mode 100644 index 0000000..86ce677 --- /dev/null +++ b/public/js/tools/url.js @@ -0,0 +1,32 @@ +// ═══════════════════════════════════════════════════════ +// URL Shortener +// ═══════════════════════════════════════════════════════ +const urlHist = []; +async function shortenURL() { + const url = document.getElementById('urlInput').value.trim(); + if (!url) return setStatus('urlStatus','error','Enter a URL.'); + const d = await apiPost('/api/url/shorten', { url }); + if (d.success) { + document.getElementById('shortUrlLink').href = d.shortUrl; + document.getElementById('shortUrlLink').textContent = d.shortUrl; + document.getElementById('urlResult').classList.add('visible'); + setStatus('urlStatus','success','Shortened ✓'); + urlHist.unshift({ short: d.shortUrl, original: url, time: new Date().toLocaleTimeString() }); + renderUrlHistory(); + } else setStatus('urlStatus','error', d.error); +} +function copyShortUrl() { copyText(document.getElementById('shortUrlLink').textContent); } +function renderUrlHistory() { + const c = document.getElementById('urlHistory'); + if (!urlHist.length) { c.textContent = 'No links shortened yet.'; return; } + c.innerHTML = urlHist.slice(0,10).map(h => ` +
    +
    ${h.short} +
    ${h.original}
    + ${h.time} +
    `).join(''); +} + +// Keyboard shortcut +document.getElementById('urlInput').addEventListener('keydown', e => { if(e.key==='Enter') shortenURL(); }); + diff --git a/public/js/tools/uuid.js b/public/js/tools/uuid.js new file mode 100644 index 0000000..06c0962 --- /dev/null +++ b/public/js/tools/uuid.js @@ -0,0 +1,14 @@ +// ═══════════════════════════════════════════════════════ +// UUID +// ═══════════════════════════════════════════════════════ +let lastUUIDs = []; +async function generateUUIDs() { + const d = await apiPost('/api/uuid', { count: parseInt(document.getElementById('uuidCount').value) }); + if (d.success) { + lastUUIDs = d.uuids; + document.getElementById('uuidResults').innerHTML = d.uuids.map(u => + `
    ${u}
    `).join(''); + } +} +function copyUUIDs() { if (lastUUIDs.length) copyText(lastUUIDs.join('\n')); } + diff --git a/public/js/tools/youtube.js b/public/js/tools/youtube.js new file mode 100644 index 0000000..1f1d223 --- /dev/null +++ b/public/js/tools/youtube.js @@ -0,0 +1,26 @@ +// ═══════════════════════════════════════════════════════ +// YouTube +// ═══════════════════════════════════════════════════════ +async function fetchYouTube() { + const url = document.getElementById('ytInput').value.trim(); + if (!url) return setStatus('ytStatus','error','Enter a YouTube URL.'); + setStatus('ytStatus','info','Fetching...'); + const d = await apiPost('/api/youtube/info', { url }); + if (d.success) { + document.getElementById('ytThumb').src = d.thumbnail; + document.getElementById('ytThumb').onerror = function() { this.src = d.thumbnailHQ; }; + document.getElementById('ytTitle').textContent = d.title; + document.getElementById('ytAuthor').textContent = d.author; + document.getElementById('ytAuthor').href = d.authorUrl; + document.getElementById('ytThumbMax').textContent = d.thumbnail; + document.getElementById('ytThumbHQ').textContent = d.thumbnailHQ; + document.getElementById('ytEmbed').value = ``; + document.getElementById('ytWatch').href = d.watchUrl; + document.getElementById('ytResult').classList.add('visible'); + setStatus('ytStatus','success','Fetched ✓'); + } else { document.getElementById('ytResult').classList.remove('visible'); setStatus('ytStatus','error', d.error); } +} + +// Keyboard shortcut +document.getElementById('ytInput').addEventListener('keydown', e => { if(e.key==='Enter') fetchYouTube(); }); + diff --git a/public/tools/ascii.html b/public/tools/ascii.html new file mode 100644 index 0000000..a694ff2 --- /dev/null +++ b/public/tools/ascii.html @@ -0,0 +1,38 @@ + +
    + +
    +

    ASCII Art Generator

    +

    Convert text into ASCII art banners using block characters.

    +
    +
    +
    Input Text
    + +
    + +
    +
    Output
    + +
    + +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/ascii/generate +
    Generate ASCII art text from a string (A-Z, 0-9, common punctuation).
    +
    const res = await fetch(`${BASE_URL}/api/ascii/generate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "HI" }) +}); +// → { success: true, result: "█ █ █████\n█ █ █ \n████ █ \n█ █ █ \n█ █ █████" }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/base64.html b/public/tools/base64.html new file mode 100644 index 0000000..27a6686 --- /dev/null +++ b/public/tools/base64.html @@ -0,0 +1,54 @@ + +
    + +
    +

    Base64 Encoder / Decoder

    +

    Encode or decode Base64 strings.

    +
    +
    +
    +
    Input
    + +
    + + +
    +
    +
    +
    Output
    + +
    +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/base64/encode +
    Encode a string to Base64.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/base64/encode`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "Hello World" }) +}); +const data = await res.json(); +// → { success: true, result: "SGVsbG8gV29ybGQ=" }
    +
    +
    + POST/api/base64/decode +
    Decode a Base64 string back to text.
    +
    await fetch(`${BASE_URL}/api/base64/decode`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "SGVsbG8gV29ybGQ=" }) +}); +// → { success: true, result: "Hello World" }
    +
    +
    +
    +
    + diff --git a/public/tools/byteconv.html b/public/tools/byteconv.html new file mode 100644 index 0000000..8949d57 --- /dev/null +++ b/public/tools/byteconv.html @@ -0,0 +1,50 @@ + +
    + +
    +

    Byte Size Converter

    +

    Convert between bytes, KB, MB, GB, TB with binary (1024) and SI (1000) modes.

    +
    +
    +
    +
    +
    Value
    + +
    +
    +
    Unit
    + +
    +
    +
    + + +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/bytes/convert +
    Convert a value between byte units. Use mode: "binary" (1024) or "si" (1000).
    +
    const res = await fetch(`${BASE_URL}/api/bytes/convert`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ value: 1, unit: "GB", mode: "binary" }) +}); +// → { success: true, B: 1073741824, KB: 1048576, MB: 1024, GB: 1, TB: 0.000977, PB: 0.00000095 }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/caseconv.html b/public/tools/caseconv.html new file mode 100644 index 0000000..b7ed977 --- /dev/null +++ b/public/tools/caseconv.html @@ -0,0 +1,40 @@ + +
    + +
    +

    Case Converter

    +

    Convert text between different cases.

    +
    +
    +
    Input Text
    + +
    + +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/text/case +
    Convert text to all case variations at once.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/text/case`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "Hello World Example" }) +}); +const data = await res.json(); +// → { success: true, uppercase: "HELLO WORLD EXAMPLE", +// lowercase: "hello world example", titleCase: "Hello World Example", +// camelCase: "helloWorldExample", snakeCase: "hello_world_example", +// kebabCase: "hello-world-example", dotCase: "hello.world.example", +// reversed: "elpmaxE dlroW olleH" }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/chmod.html b/public/tools/chmod.html new file mode 100644 index 0000000..ba3b7c5 --- /dev/null +++ b/public/tools/chmod.html @@ -0,0 +1,45 @@ + +
    + +
    +

    Chmod Calculator

    +

    Convert between numeric (755) and symbolic (rwxr-xr-x) Unix file permissions.

    +
    +
    +
    +
    +
    Numeric (Octal)
    + +
    +
    +
    Symbolic
    + +
    +
    +
    Permission Matrix
    +
    +
    Command
    +
    +
    CHMOD
    +
    chmod 755 filename
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/chmod/calculate +
    Convert between numeric and symbolic chmod. Pass numeric or symbolic.
    +
    const res = await fetch(`${BASE_URL}/api/chmod/calculate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ numeric: "755" }) +}); +// → { success: true, numeric: "755", symbolic: "rwxr-xr-x", owner: {...}, group: {...}, others: {...} }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/color.html b/public/tools/color.html new file mode 100644 index 0000000..0d41f2e --- /dev/null +++ b/public/tools/color.html @@ -0,0 +1,43 @@ + +
    + +
    +

    Color Converter

    +

    Convert colors between HEX, RGB, and HSL with a live preview.

    +
    +
    +
    +
    +
    Color Value
    + +
    + + +
    +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/color/convert +
    Convert a color between HEX, RGB, and HSL formats.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/color/convert`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ color: "#6c63ff" }) + // Also accepts: "rgb(108,99,255)" or "hsl(245,100,69)" +}); +const data = await res.json(); +// → { success: true, hex: "#6c63ff", rgb: "rgb(108, 99, 255)", +// hsl: "hsl(243, 100%, 69%)", r: 108, g: 99, b: 255 }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/counter.html b/public/tools/counter.html new file mode 100644 index 0000000..8f30a5b --- /dev/null +++ b/public/tools/counter.html @@ -0,0 +1,34 @@ + +
    + +
    +

    Word & Character Counter

    +

    Get detailed text statistics as you type.

    +
    +
    + +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/text/stats +
    Get detailed text statistics: characters, words, sentences, reading time, and top character frequency.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/text/stats`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "Hello world. This is a test." }) +}); +const data = await res.json(); +// → { success: true, characters: 28, charactersNoSpaces: 23, +// words: 6, sentences: 2, paragraphs: 1, lines: 1, +// readingTime: "1 min", topChars: [["l",3], ["s",3], ...] }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/cron.html b/public/tools/cron.html new file mode 100644 index 0000000..94d661d --- /dev/null +++ b/public/tools/cron.html @@ -0,0 +1,47 @@ + +
    + +
    +

    Cron Expression Parser

    +

    Parse cron expressions into human-readable descriptions and see next run times.

    +
    +
    +
    Cron Expression
    + +
    + + + + + + +
    +
    + +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL, e.g. https://winnieapi-v2.yourdomain.com
    +
    + POST/api/cron/parse +
    Parse a cron expression into a human-readable description and next run times.
    +
    const res = await fetch(`${BASE_URL}/api/cron/parse`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ expression: "*/5 * * * *" }) +}); +const data = await res.json(); +// → { success: true, description: "Every 5 minutes", fields: {...}, nextRuns: [...] }
    +
    +
    +
    +
    + diff --git a/public/tools/cssmin.html b/public/tools/cssmin.html new file mode 100644 index 0000000..feff6b8 --- /dev/null +++ b/public/tools/cssmin.html @@ -0,0 +1,44 @@ + +
    + +
    +

    CSS Minifier

    +

    Minify CSS by removing comments and whitespace.

    +
    +
    +
    +
    Input CSS
    + +
    +
    +
    +
    Minified Output
    + +
    +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/css/minify +
    Minify CSS by removing comments, whitespace, and unnecessary characters. Returns size savings.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/css/minify`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + css: ".container {\n display: flex;\n /* comment */\n gap: 16px;\n}" + }) +}); +const data = await res.json(); +// → { success: true, result: ".container{display:flex;gap:16px}", +// original: 58, minified: 33, saved: 25, percentage: 43 }
    +
    +
    +
    +
    + diff --git a/public/tools/diff.html b/public/tools/diff.html new file mode 100644 index 0000000..12c12d7 --- /dev/null +++ b/public/tools/diff.html @@ -0,0 +1,52 @@ + +
    + +
    +

    Diff Checker

    +

    Compare two texts and find the differences.

    +
    +
    +
    +
    Original
    + +
    +
    +
    Modified
    + +
    +
    +
    + +
    +
    +
    + +
    +
    ℹ️ Diff Checker runs entirely client-side. No server API is needed. Here's the line-by-line comparison logic:
    +
    +
    Simple line-by-line diff in JavaScript.
    +
    // Client-side diff — no API call needed +function diffLines(original, modified) { + const a = original.split('\n'); + const b = modified.split('\n'); + const result = []; + const max = Math.max(a.length, b.length); + for (let i = 0; i < max; i++) { + if (a[i] === undefined) result.push({ type: 'add', line: b[i] }); + else if (b[i] === undefined) result.push({ type: 'del', line: a[i] }); + else if (a[i] === b[i]) result.push({ type: 'same', line: a[i] }); + else { + result.push({ type: 'del', line: a[i] }); + result.push({ type: 'add', line: b[i] }); + } + } + return result; +} + +const diff = diffLines("hello\nworld", "hello\nearth"); +// → [{ type: "same", line: "hello" }, { type: "del", line: "world" }, { type: "add", line: "earth" }]
    +
    +
    +
    +
    + diff --git a/public/tools/envjson.html b/public/tools/envjson.html new file mode 100644 index 0000000..1a36283 --- /dev/null +++ b/public/tools/envjson.html @@ -0,0 +1,57 @@ + +
    + +
    +

    ENV ↔ JSON Converter

    +

    Convert between .env files and JSON objects.

    +
    +
    +
    +
    .env Input
    + +
    + +
    +
    +
    +
    JSON Input
    + +
    + +
    +
    +
    +
    Output
    + +
    + +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/convert/env-to-json +
    Convert .env format text to a JSON object.
    +
    const res = await fetch(`${BASE_URL}/api/convert/env-to-json`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ env: "DB_HOST=localhost\nDB_PORT=5432" }) +}); +// → { success: true, result: { DB_HOST: "localhost", DB_PORT: "5432" } }
    +
    +
    + POST/api/convert/json-to-env +
    Convert a flat JSON object to .env format text.
    +
    const res = await fetch(`${BASE_URL}/api/convert/json-to-env`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ json: '{"DB_HOST":"localhost","DB_PORT":"5432"}' }) +}); +// → { success: true, result: "DB_HOST=localhost\nDB_PORT=5432" }
    +
    +
    +
    +
    + diff --git a/public/tools/escape.html b/public/tools/escape.html new file mode 100644 index 0000000..ecc07d4 --- /dev/null +++ b/public/tools/escape.html @@ -0,0 +1,54 @@ + +
    + +
    +

    String Escape / Unescape

    +

    Escape and unescape special characters in strings.

    +
    +
    +
    +
    Input
    + +
    + + +
    +
    +
    +
    Output
    + +
    +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/escape +
    Escape special characters in a string (backslash sequences like \n, \t, \", etc).
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/escape`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: 'Hello "World"\nNew line' }) +}); +const data = await res.json(); +// → { success: true, result: "Hello \\\"World\\\"\\nNew line" }
    +
    +
    + POST/api/unescape +
    Unescape backslash sequences back to their original characters.
    +
    await fetch(`${BASE_URL}/api/unescape`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: 'Hello \\\"World\\\"\\nNew line' }) +}); +// → { success: true, result: 'Hello "World"\nNew line' }
    +
    +
    +
    +
    + diff --git a/public/tools/hash.html b/public/tools/hash.html new file mode 100644 index 0000000..72cafe4 --- /dev/null +++ b/public/tools/hash.html @@ -0,0 +1,45 @@ + +
    + +
    +

    Hash Generator

    +

    Generate cryptographic hashes from any text.

    +
    +
    +
    Input Text
    + +
    + +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/hash +
    Generate MD5, SHA-1, SHA-256, and SHA-512 hashes. Optionally specify a single algorithm.
    +
    const BASE_URL = "http://localhost:3000"; + +// Get all hashes at once: +const res = await fetch(`${BASE_URL}/api/hash`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "hello world" }) +}); +// → { success: true, hashes: { md5: "5eb6...", sha1: "2aae...", sha256: "b94d...", sha512: "309e..." } } + +// Or get a single algorithm: +await fetch(`${BASE_URL}/api/hash`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "hello", algorithm: "sha256" }) +}); +// → { success: true, hashes: { sha256: "2cf2..." } }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/hmac.html b/public/tools/hmac.html new file mode 100644 index 0000000..3bdc613 --- /dev/null +++ b/public/tools/hmac.html @@ -0,0 +1,49 @@ + +
    + +
    +

    HMAC Generator

    +

    Generate HMAC signatures from a message and secret key.

    +
    +
    +
    Message
    + +
    Secret Key
    + +
    + + +
    +
    + +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/hmac +
    Generate an HMAC signature. Supports sha256, sha512, sha1, md5.
    +
    const res = await fetch(`${BASE_URL}/api/hmac`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + message: "hello world", + secret: "my-secret-key", + algorithm: "sha256" + }) +}); +// → { success: true, hmac: "734c...", algorithm: "sha256" }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/htmlent.html b/public/tools/htmlent.html new file mode 100644 index 0000000..7415be6 --- /dev/null +++ b/public/tools/htmlent.html @@ -0,0 +1,53 @@ + +
    + +
    +

    HTML Entity Encoder / Decoder

    +

    Convert special characters to HTML entities and back.

    +
    +
    +
    +
    Input
    + +
    + + +
    +
    +
    +
    Output
    + +
    +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/html/encode +
    Convert special characters to HTML entities.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/html/encode`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: '<div class="test">&</div>' }) +}); +// → { success: true, result: "&lt;div class=&quot;test&quot;&gt;..." }
    +
    +
    + POST/api/html/decode +
    Decode HTML entities back to characters.
    +
    await fetch(`${BASE_URL}/api/html/decode`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "&lt;p&gt;Hello&lt;/p&gt;" }) +}); +// → { success: true, result: "<p>Hello</p>" }
    +
    +
    +
    +
    + diff --git a/public/tools/httpstatus.html b/public/tools/httpstatus.html new file mode 100644 index 0000000..33f583d --- /dev/null +++ b/public/tools/httpstatus.html @@ -0,0 +1,43 @@ + +
    + +
    +

    HTTP Status Codes

    +

    Quick reference for all HTTP status codes with descriptions and categories.

    +
    +
    +
    Search / Lookup
    + +
    + + + + + + +
    +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL, e.g. https://winnieapi-v2.yourdomain.com
    +
    + GET/api/http-status +
    Get all HTTP status codes with their text and category.
    +
    const res = await fetch(`${BASE_URL}/api/http-status`); +const data = await res.json(); +// → { success: true, statuses: [{ code: 200, text: "OK", category: "2xx" }, ...] }
    +
    +
    + GET/api/http-status/:code +
    Look up a specific HTTP status code.
    +
    const res = await fetch(`${BASE_URL}/api/http-status/404`); +const data = await res.json(); +// → { success: true, code: 404, text: "Not Found", category: "4xx" }
    +
    +
    +
    +
    + diff --git a/public/tools/ip.html b/public/tools/ip.html new file mode 100644 index 0000000..9bea3ae --- /dev/null +++ b/public/tools/ip.html @@ -0,0 +1,43 @@ + +
    + +
    +

    IP Address Lookup

    +

    Get geolocation and network info for any IP address.

    +
    +
    +
    + + + +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + GET/api/ip +
    Get geolocation info for the server's public IP.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/ip`); +const data = await res.json(); +// → { success: true, query: "203.0.113.1", country: "United States", +// regionName: "California", city: "San Jose", timezone: "America/Los_Angeles", +// isp: "Example ISP", ... }
    +
    +
    + GET/api/ip/:ip +
    Lookup a specific IP address.
    +
    const res = await fetch(`${BASE_URL}/api/ip/8.8.8.8`); +const data = await res.json(); +// → { success: true, query: "8.8.8.8", country: "United States", +// city: "Ashburn", isp: "Google LLC", ... }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/json.html b/public/tools/json.html new file mode 100644 index 0000000..5cb1da2 --- /dev/null +++ b/public/tools/json.html @@ -0,0 +1,77 @@ + +
    + +
    +

    JSON Formatter

    +

    Paste your JSON below to beautify, minify, or validate it.

    +
    +
    +
    +
    Input
    + +
    + + + + +
    +
    + + +
    +
    +
    +
    Output
    + +
    + +
    +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL, e.g. https://winnieapi-v2.yourdomain.com
    +
    + POST/api/json/format +
    Beautify JSON with configurable indentation.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/json/format`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + json: '{"name":"WinnieAPI-v2","version":1}', + indent: 2 // 2, 4, or "\t" + }) +}); +const data = await res.json(); +// → { success: true, result: "{\n \"name\": \"WinnieAPI-v2\",\n ...}" }
    +
    +
    + POST/api/json/minify +
    Minify JSON to a single line.
    +
    await fetch(`${BASE_URL}/api/json/minify`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ json: '{ "a" : 1 , "b" : 2 }' }) +}); +// → { success: true, result: '{"a":1,"b":2}' }
    +
    +
    + POST/api/json/validate +
    Check if a string is valid JSON.
    +
    await fetch(`${BASE_URL}/api/json/validate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ json: '{"valid": true}' }) +}); +// → { success: true, valid: true, message: "Valid JSON ✓" }
    +
    +
    +
    +
    + diff --git a/public/tools/jsoncsv.html b/public/tools/jsoncsv.html new file mode 100644 index 0000000..140c87a --- /dev/null +++ b/public/tools/jsoncsv.html @@ -0,0 +1,58 @@ + +
    + +
    +

    JSON ↔ CSV Converter

    +

    Convert between JSON arrays and CSV format.

    +
    +
    +
    +
    JSON Input
    + +
    + +
    +
    +
    +
    CSV Input
    + +
    + +
    +
    +
    +
    Output
    + +
    + + +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL, e.g. https://winnieapi-v2.yourdomain.com
    +
    + POST/api/convert/json-to-csv +
    Convert a JSON array to CSV format.
    +
    const res = await fetch(`${BASE_URL}/api/convert/json-to-csv`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ json: '[{"name":"Alice","age":30}]' }) +}); +// → { success: true, result: "name,age\nAlice,30" }
    +
    +
    + POST/api/convert/csv-to-json +
    Convert CSV text to a JSON array.
    +
    const res = await fetch(`${BASE_URL}/api/convert/csv-to-json`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ csv: "name,age\nAlice,30" }) +}); +// → { success: true, result: [{"name":"Alice","age":"30"}] }
    +
    +
    +
    +
    + diff --git a/public/tools/jwt.html b/public/tools/jwt.html new file mode 100644 index 0000000..ba13f91 --- /dev/null +++ b/public/tools/jwt.html @@ -0,0 +1,40 @@ + +
    + +
    +

    JWT Decoder

    +

    Decode and inspect JSON Web Tokens.

    +
    +
    +
    JWT Token
    + +
    + +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/jwt/decode +
    Decode a JWT token and inspect its header, payload, and expiry.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/jwt/decode`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U" + }) +}); +const data = await res.json(); +// → { success: true, header: { alg: "HS256", typ: "JWT" }, +// payload: { sub: "1234567890" }, expired: null, signature: "..." }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/lorem.html b/public/tools/lorem.html new file mode 100644 index 0000000..f932264 --- /dev/null +++ b/public/tools/lorem.html @@ -0,0 +1,37 @@ + +
    + +
    +

    Lorem Ipsum Generator

    +

    Generate placeholder text.

    +
    +
    +
    + + + + +
    + +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/lorem +
    Generate lorem ipsum placeholder text.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/lorem`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ paragraphs: 3 }) // 1–20 +}); +const data = await res.json(); +// → { success: true, result: "Lorem ipsum dolor sit amet..." }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/markdown.html b/public/tools/markdown.html new file mode 100644 index 0000000..61b0119 --- /dev/null +++ b/public/tools/markdown.html @@ -0,0 +1,42 @@ + +
    + +
    +

    Markdown Preview

    +

    Write Markdown and see a live preview.

    +
    +
    +
    +
    Markdown
    + +
    +
    +
    Preview
    +
    Preview will appear here...
    +
    +
    +
    + +
    +
    ℹ️ Markdown Preview runs entirely client-side using regex-based rendering. No server API is needed. Here's how to use a similar approach in your own code:
    +
    +
    Simple Markdown to HTML conversion in JavaScript.
    +
    // Client-side Markdown rendering — no API call needed +function renderMarkdown(md) { + return md + .replace(/^### (.+)$/gm, '<h3>$1</h3>') + .replace(/^## (.+)$/gm, '<h2>$1</h2>') + .replace(/^# (.+)$/gm, '<h1>$1</h1>') + .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') + .replace(/\*(.+?)\*/g, '<em>$1</em>') + .replace(/`([^`]+)`/g, '<code>$1</code>') + .replace(/\n\n/g, '</p><p>'); +} + +const html = renderMarkdown("# Hello\n\n**Bold** and *italic*"); +// → "<h1>Hello</h1></p><p><strong>Bold</strong> and <em>italic</em>"
    +
    +
    +
    +
    + diff --git a/public/tools/numbase.html b/public/tools/numbase.html new file mode 100644 index 0000000..f93c770 --- /dev/null +++ b/public/tools/numbase.html @@ -0,0 +1,52 @@ + +
    + +
    +

    Number Base Converter

    +

    Convert numbers between different bases.

    +
    +
    +
    +
    +
    Number
    + +
    +
    +
    From Base
    + +
    + +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/number/convert +
    Convert a number between decimal, binary, octal, and hex bases.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/number/convert`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + value: "255", + fromBase: 10 // 2, 8, 10, or 16 + }) +}); +const data = await res.json(); +// → { success: true, decimal: "255", binary: "11111111", +// octal: "377", hex: "FF" }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/password.html b/public/tools/password.html new file mode 100644 index 0000000..23e978b --- /dev/null +++ b/public/tools/password.html @@ -0,0 +1,54 @@ + +
    + +
    +

    Password Generator

    +

    Generate cryptographically secure passwords.

    +
    +
    +
    Click "Generate" to create a password
    +
    +
    + + +
    +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/password +
    Generate cryptographically secure passwords with configurable rules.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/password`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + length: 24, // 4–128 (default: 16) + uppercase: true, // include A-Z + lowercase: true, // include a-z + numbers: true, // include 0-9 + symbols: true, // include !@#$%... + count: 5 // 1–20 passwords at once + }) +}); +const data = await res.json(); +// → { success: true, passwords: ["xK#9mL...", "Qw!7pR...", ...] }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/placeholder.html b/public/tools/placeholder.html new file mode 100644 index 0000000..5e2f17e --- /dev/null +++ b/public/tools/placeholder.html @@ -0,0 +1,80 @@ + +
    + +
    +

    Placeholder Image Generator

    +

    Generate custom placeholder images with text, colors, and dimensions.

    +
    +
    +
    +
    Settings
    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    +
    + + + 28px +
    +
    + + +
    +
    +
    +
    Preview
    + +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL, e.g. https://winnieapi-v2.yourdomain.com
    +
    + GET/api/placeholder/:width/:height +
    Generate a placeholder image (SVG). Supports query params: bg, fg, text, fontSize.
    +
    // Returns an SVG image directly — use as an <img> src + +// Basic usage: +<img src="${BASE_URL}/api/placeholder/400/300" /> + +// Custom colors & text: +<img src="${BASE_URL}/api/placeholder/800/400?bg=1a1a25&fg=6c63ff&text=Hero+Image&fontSize=36" /> + +// Fetch as SVG text: +const res = await fetch(`${BASE_URL}/api/placeholder/200/200?text=Avatar`); +const svg = await res.text(); +// → <svg xmlns=...>...</svg>
    +
    +
    +
    +
    + diff --git a/public/tools/qrcode.html b/public/tools/qrcode.html new file mode 100644 index 0000000..572fdf8 --- /dev/null +++ b/public/tools/qrcode.html @@ -0,0 +1,43 @@ + +
    + +
    +

    QR Code Generator

    +

    Generate QR codes from any text or URL.

    +
    +
    +
    Data
    + +
    + +
    Type something above to generate a QR code
    +
    +
    + +
    +
    + +
    +
    ℹ️ QR Code generation runs entirely client-side using the qrcode-generator library. No server API is needed.
    +
    +
    Generate QR codes using the qrcode-generator package (works in Node.js and browsers).
    +
    // Install: npm install qrcode-generator +// Browser CDN: https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js + +const qr = qrcode(0, 'M'); +qr.addData("https://example.com"); +qr.make(); + +// Render as an <img> tag: +document.getElementById('container').innerHTML = + qr.createImgTag(4, 8); + +// Or get a data URL: +const dataUrl = qr.createDataURL(4, 8); +// → "data:image/gif;base64,..."
    +
    +
    +
    +
    +
    + diff --git a/public/tools/qrreader.html b/public/tools/qrreader.html new file mode 100644 index 0000000..2287ddb --- /dev/null +++ b/public/tools/qrreader.html @@ -0,0 +1,90 @@ + +
    + +
    +

    QR Code Reader

    +

    Scan QR codes using your camera or upload an image file.

    +
    + +
    + +
    +
    Scan Method
    +
    + + + +
    + + + + + + +
    + + +
    +
    Scan Result
    +
    + + Point your camera at a QR code or upload an image to scan. +
    + +
    +
    + +
    +
    + +
    +
    ℹ️ QR Code reading runs entirely client-side using the jsQR library. No server API is needed.
    +
    +
    Decode QR codes from image data using the jsQR library (works in Node.js and browsers).
    +
    // Install: npm install jsqr +// Browser CDN: https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js + +// Read from a canvas element: +const canvas = document.getElementById('myCanvas'); +const ctx = canvas.getContext('2d'); +const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + +const code = jsQR(imageData.data, imageData.width, imageData.height); +if (code) { + console.log("Decoded:", code.data); + // → "https://example.com" +} + +// Read from camera (MediaDevices API): +const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); +const video = document.createElement('video'); +video.srcObject = stream; +video.play(); +// Then draw video frames to canvas and scan with jsQR
    +
    +
    +
    +
    + diff --git a/public/tools/regex.html b/public/tools/regex.html new file mode 100644 index 0000000..9466d9a --- /dev/null +++ b/public/tools/regex.html @@ -0,0 +1,48 @@ + +
    + +
    +

    Regex Tester

    +

    Test regular expressions with live matching.

    +
    +
    +
    +
    +
    Pattern
    + +
    +
    +
    Flags
    + +
    +
    +
    Test String
    + +
    Matches
    +
    +
    +
    + +
    +
    ℹ️ Regex Tester runs entirely client-side using JavaScript's native RegExp. No server API is needed. Here's how to use the same logic in your own code:
    +
    +
    Test a regex pattern against a string in JavaScript.
    +
    // Client-side regex testing — no API call needed +const pattern = "[a-z]+@[a-z]+\\.[a-z]+"; +const flags = "gi"; +const testString = "Contact us at hello@example.com or info@test.org"; + +const regex = new RegExp(pattern, flags); +const matches = [...testString.matchAll(regex)]; + +matches.forEach((m, i) => { + console.log(`Match ${i+1}: ${m[0]} (index: ${m.index})`); +}); +// → Match 1: hello@example.com (index: 14) +// → Match 2: info@test.org (index: 36)
    +
    +
    +
    +
    +
    + diff --git a/public/tools/slugify.html b/public/tools/slugify.html new file mode 100644 index 0000000..696f03e --- /dev/null +++ b/public/tools/slugify.html @@ -0,0 +1,53 @@ + +
    + +
    +

    Slug Generator

    +

    Convert any text into a clean, URL-friendly slug.

    +
    +
    +
    Input Text
    + +
    + + + +
    +
    Slug Output
    +
    +
    SLUG
    +
    +
    +
    + +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/text/slugify +
    Convert text to a URL-friendly slug. Optional: separator (-, _, .) and lowercase (boolean).
    +
    const res = await fetch(`${BASE_URL}/api/text/slugify`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + text: "Hello World! My Blog Post #1", + separator: "-", + lowercase: true + }) +}); +// → { success: true, result: "hello-world-my-blog-post-1" }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/sqlformat.html b/public/tools/sqlformat.html new file mode 100644 index 0000000..a18c79f --- /dev/null +++ b/public/tools/sqlformat.html @@ -0,0 +1,54 @@ + +
    + +
    +

    SQL Formatter

    +

    Beautify and minify SQL queries with keyword highlighting.

    +
    +
    +
    +
    Input SQL
    + +
    + + + +
    +
    +
    +
    Output
    + +
    + +
    +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/sql/format +
    Beautify a SQL query with indentation.
    +
    const res = await fetch(`${BASE_URL}/api/sql/format`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sql: "SELECT * FROM users WHERE active = 1" }) +}); +// → { success: true, result: "SELECT\n *\nFROM\n users\nWHERE\n active = 1" }
    +
    +
    + POST/api/sql/minify +
    Minify a SQL query to a single line.
    +
    await fetch(`${BASE_URL}/api/sql/minify`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sql: "SELECT\n *\nFROM\n users" }) +}); +// → { success: true, result: "SELECT * FROM users" }
    +
    +
    +
    +
    + diff --git a/public/tools/textenc.html b/public/tools/textenc.html new file mode 100644 index 0000000..e23431d --- /dev/null +++ b/public/tools/textenc.html @@ -0,0 +1,44 @@ + +
    + +
    +

    Text Encoder / Decoder

    +

    Encode and decode text in ROT13, Binary, Morse Code, and more.

    +
    +
    +
    Input Text
    + +
    + + + + + + +
    +
    +
    Output
    + +
    + + +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL, e.g. https://winnieapi-v2.yourdomain.com
    +
    + POST/api/text/encode +
    Encode text with the specified method (rot13, binary, morse, reverse, leetspeak, upside_down).
    +
    const res = await fetch(`${BASE_URL}/api/text/encode`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "Hello World", method: "morse" }) +}); +// → { success: true, result: ".... . .-.. .-.. --- / .-- --- .-. .-.. -.." }
    +
    +
    +
    +
    + diff --git a/public/tools/timestamp.html b/public/tools/timestamp.html new file mode 100644 index 0000000..5306b5e --- /dev/null +++ b/public/tools/timestamp.html @@ -0,0 +1,42 @@ + +
    + +
    +

    Timestamp Converter

    +

    Convert between Unix timestamps and human-readable dates.

    +
    +
    +
    Input (Unix timestamp, ISO date, or "now")
    +
    + + + +
    +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/timestamp +
    Convert between Unix timestamps, ISO dates, and relative time. Accepts 10-digit (seconds), 13-digit (ms), ISO strings, or "now".
    +
    const BASE_URL = "http://localhost:3000"; + +// From Unix timestamp: +const res = await fetch(`${BASE_URL}/api/timestamp`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ value: "1709683200" }) + // Also: "now", "2024-03-06", or "1709683200000" (ms) +}); +const data = await res.json(); +// → { success: true, unix: 1709683200, unixMs: 1709683200000, +// iso: "2024-03-06T00:00:00.000Z", utc: "Wed, 06 Mar 2024...", +// local: "...", relative: "1y ago" }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/url.html b/public/tools/url.html new file mode 100644 index 0000000..8ea6dbc --- /dev/null +++ b/public/tools/url.html @@ -0,0 +1,60 @@ + +
    + +
    +

    URL Shortener

    +

    Paste a long URL and get a short, shareable link.

    +
    +
    +
    Long URL
    +
    + + +
    +
    +
    +
    +
    Your Short URL
    +
    + + +
    +
    +
    +
    Recent Links
    No links shortened yet.
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/url/shorten +
    Create a shortened URL.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/url/shorten`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: "https://github.com/long/repo/path" }) +}); +const data = await res.json(); +// → { success: true, shortUrl: "http://localhost:3000/s/abc1234", id: "abc1234" }
    +
    +
    + GET/api/url/stats/:id +
    Get click stats for a shortened URL.
    +
    const res = await fetch(`${BASE_URL}/api/url/stats/abc1234`); +const data = await res.json(); +// → { success: true, url: "https://...", clicks: 42, createdAt: "..." }
    +
    +
    + GET/s/:id +
    Redirect to the original URL (use in browser).
    +
    // Simply open in browser or use as a link: +window.open(`${BASE_URL}/s/abc1234`); +// → 302 redirect to the original URL
    +
    +
    +
    +
    +
    + diff --git a/public/tools/uuid.html b/public/tools/uuid.html new file mode 100644 index 0000000..47f4cde --- /dev/null +++ b/public/tools/uuid.html @@ -0,0 +1,37 @@ + +
    + +
    +

    UUID Generator

    +

    Generate random v4 UUIDs.

    +
    +
    +
    + + + + +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/uuid +
    Generate random v4 UUIDs. Batch up to 100.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/uuid`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ count: 5 }) // 1–100 +}); +const data = await res.json(); +// → { success: true, uuids: ["550e8400-e29b-41d4-...", ...] }
    +
    +
    +
    +
    +
    + diff --git a/public/tools/youtube.html b/public/tools/youtube.html new file mode 100644 index 0000000..23b2969 --- /dev/null +++ b/public/tools/youtube.html @@ -0,0 +1,58 @@ + +
    + +
    +

    YouTube Tool

    +

    Extract metadata, thumbnails, and embed codes from any YouTube video.

    +
    +
    +
    YouTube URL
    +
    + + +
    +
    +
    + Thumbnail +
    +

    +
    by
    +
    Thumbnail URLs (click to copy)
    +
    + + +
    +
    Embed Code
    + + +
    +
    +
    + +
    +
    All examples use BASE_URL — set it to your deployment URL.
    +
    + POST/api/youtube/info +
    Extract metadata, thumbnails, and embed info from a YouTube video.
    +
    const BASE_URL = "http://localhost:3000"; + +const res = await fetch(`${BASE_URL}/api/youtube/info`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + }) +}); +const data = await res.json(); +// → { success: true, videoId: "dQw4w9WgXcQ", title: "...", +// author: "...", thumbnail: "https://img.youtube.com/...", +// embedUrl: "https://youtube.com/embed/...", ... }
    +
    +
    +
    +
    +
    + diff --git a/server.js b/server.js new file mode 100644 index 0000000..04c7076 --- /dev/null +++ b/server.js @@ -0,0 +1,968 @@ +const express = require('express'); +const path = require('path'); +const crypto = require('crypto'); +const { nanoid } = require('nanoid'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// ─── Middleware ──────────────────────────────────────────────────────────────── +app.use(express.json({ limit: '5mb' })); +app.use(express.urlencoded({ extended: true })); +app.use(express.static(path.join(__dirname, 'public'))); + +// ─── In-memory URL store ────────────────────────────────────────────────────── +const urlStore = new Map(); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: JSON Formatter +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/json/format', (req, res) => { + try { + const { json, indent } = req.body; + const parsed = JSON.parse(json); + const formatted = JSON.stringify(parsed, null, indent || 2); + res.json({ success: true, result: formatted }); + } catch (err) { + res.json({ success: false, error: err.message }); + } +}); + +app.post('/api/json/minify', (req, res) => { + try { + const { json } = req.body; + const parsed = JSON.parse(json); + const minified = JSON.stringify(parsed); + res.json({ success: true, result: minified }); + } catch (err) { + res.json({ success: false, error: err.message }); + } +}); + +app.post('/api/json/validate', (req, res) => { + try { + const { json } = req.body; + JSON.parse(json); + res.json({ success: true, valid: true, message: 'Valid JSON ✓' }); + } catch (err) { + res.json({ success: true, valid: false, message: err.message }); + } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: URL Shortener +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/url/shorten', (req, res) => { + try { + const { url } = req.body; + if (!url || !url.match(/^https?:\/\/.+/)) { + return res.json({ success: false, error: 'Please enter a valid URL starting with http:// or https://' }); + } + const id = nanoid(7); + urlStore.set(id, { url, createdAt: new Date(), clicks: 0 }); + const shortUrl = `${req.protocol}://${req.get('host')}/s/${id}`; + res.json({ success: true, shortUrl, id }); + } catch (err) { + res.json({ success: false, error: err.message }); + } +}); + +app.get('/api/url/stats/:id', (req, res) => { + const entry = urlStore.get(req.params.id); + if (!entry) return res.json({ success: false, error: 'Short URL not found' }); + res.json({ success: true, ...entry, id: req.params.id }); +}); + +app.get('/s/:id', (req, res) => { + const entry = urlStore.get(req.params.id); + if (!entry) return res.status(404).send('Short URL not found'); + entry.clicks++; + res.redirect(entry.url); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: YouTube Info +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/youtube/info', async (req, res) => { + try { + const { url } = req.body; + if (!url) return res.json({ success: false, error: 'Please enter a YouTube URL' }); + const patterns = [ + /youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/, + /youtu\.be\/([a-zA-Z0-9_-]{11})/, + /youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/, + /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/ + ]; + let videoId = null; + for (const p of patterns) { + const m = url.match(p); + if (m) { videoId = m[1]; break; } + } + if (!videoId) return res.json({ success: false, error: 'Could not extract video ID from URL' }); + const oembedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`; + const response = await fetch(oembedUrl); + if (!response.ok) return res.json({ success: false, error: 'Video not found or unavailable' }); + const data = await response.json(); + res.json({ + success: true, videoId, + title: data.title, author: data.author_name, authorUrl: data.author_url, + thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`, + thumbnailHQ: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`, + embedUrl: `https://www.youtube.com/embed/${videoId}`, + watchUrl: `https://www.youtube.com/watch?v=${videoId}` + }); + } catch (err) { + res.json({ success: false, error: err.message }); + } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Hash Generator +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/hash', (req, res) => { + try { + const { text, algorithm } = req.body; + if (!text) return res.json({ success: false, error: 'Please enter some text' }); + const algos = ['md5', 'sha1', 'sha256', 'sha512']; + if (algorithm && algos.includes(algorithm)) { + const hash = crypto.createHash(algorithm).update(text, 'utf8').digest('hex'); + return res.json({ success: true, hashes: { [algorithm]: hash } }); + } + const hashes = {}; + for (const a of algos) { + hashes[a] = crypto.createHash(a).update(text, 'utf8').digest('hex'); + } + res.json({ success: true, hashes }); + } catch (err) { + res.json({ success: false, error: err.message }); + } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Base64 Encode / Decode +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/base64/encode', (req, res) => { + try { + const { text } = req.body; + res.json({ success: true, result: Buffer.from(text || '', 'utf8').toString('base64') }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); +app.post('/api/base64/decode', (req, res) => { + try { + const { text } = req.body; + res.json({ success: true, result: Buffer.from(text || '', 'base64').toString('utf8') }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: UUID Generator +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/uuid', (req, res) => { + try { + const { count } = req.body; + const n = Math.min(Math.max(parseInt(count) || 1, 1), 100); + const uuids = []; + for (let i = 0; i < n; i++) uuids.push(crypto.randomUUID()); + res.json({ success: true, uuids }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Password Generator +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/password', (req, res) => { + try { + const { length, uppercase, lowercase, numbers, symbols, count } = req.body; + const len = Math.min(Math.max(parseInt(length) || 16, 4), 128); + const n = Math.min(Math.max(parseInt(count) || 1, 1), 20); + let chars = ''; + if (uppercase !== false) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + if (lowercase !== false) chars += 'abcdefghijklmnopqrstuvwxyz'; + if (numbers !== false) chars += '0123456789'; + if (symbols) chars += '!@#$%^&*()_+-=[]{}|;:,.<>?'; + if (!chars) chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + const passwords = []; + for (let i = 0; i < n; i++) { + const bytes = crypto.randomBytes(len); + let pw = ''; + for (let j = 0; j < len; j++) pw += chars[bytes[j] % chars.length]; + passwords.push(pw); + } + res.json({ success: true, passwords }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Lorem Ipsum Generator +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/lorem', (req, res) => { + try { + const words = 'lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt culpa qui officia deserunt mollit anim id est laborum'.split(' '); + const { paragraphs } = req.body; + const n = Math.min(Math.max(parseInt(paragraphs) || 3, 1), 20); + const result = []; + for (let i = 0; i < n; i++) { + const sentenceCount = 4 + Math.floor(Math.random() * 5); + const sentences = []; + for (let s = 0; s < sentenceCount; s++) { + const wordCount = 8 + Math.floor(Math.random() * 12); + const sentence = []; + for (let w = 0; w < wordCount; w++) sentence.push(words[Math.floor(Math.random() * words.length)]); + sentence[0] = sentence[0][0].toUpperCase() + sentence[0].slice(1); + sentences.push(sentence.join(' ') + '.'); + } + result.push(sentences.join(' ')); + } + res.json({ success: true, result: result.join('\n\n') }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Color Converter +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/color/convert', (req, res) => { + try { + const { color } = req.body; + if (!color) return res.json({ success: false, error: 'No color provided' }); + let r, g, b; + const hexMatch = color.match(/^#?([0-9a-f]{3,8})$/i); + if (hexMatch) { + let hex = hexMatch[1]; + if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; + r = parseInt(hex.slice(0,2), 16); + g = parseInt(hex.slice(2,4), 16); + b = parseInt(hex.slice(4,6), 16); + } + const rgbMatch = color.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i); + if (rgbMatch) { r = +rgbMatch[1]; g = +rgbMatch[2]; b = +rgbMatch[3]; } + const hslMatch = color.match(/hsl\s*\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*\)/i); + if (hslMatch) { + const h = +hslMatch[1] / 360, s = +hslMatch[2] / 100, l = +hslMatch[3] / 100; + if (s === 0) { r = g = b = Math.round(l * 255); } + else { + const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; }; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = Math.round(hue2rgb(p, q, h + 1/3) * 255); + g = Math.round(hue2rgb(p, q, h) * 255); + b = Math.round(hue2rgb(p, q, h - 1/3) * 255); + } + } + if (r === undefined) return res.json({ success: false, error: 'Unrecognized format. Use HEX (#ff0000), RGB (rgb(255,0,0)), or HSL (hsl(0,100,50))' }); + const rn = r/255, gn = g/255, bn = b/255; + const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn); + let h, s, l = (max + min) / 2; + if (max === min) { h = s = 0; } + else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case rn: h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6; break; + case gn: h = ((bn - rn) / d + 2) / 6; break; + case bn: h = ((rn - gn) / d + 4) / 6; break; + } + } + const hex = '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join(''); + res.json({ + success: true, + hex, rgb: `rgb(${r}, ${g}, ${b})`, + hsl: `hsl(${Math.round(h*360)}, ${Math.round(s*100)}%, ${Math.round(l*100)}%)`, + r, g, b + }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: JWT Decoder +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/jwt/decode', (req, res) => { + try { + const { token } = req.body; + if (!token) return res.json({ success: false, error: 'No token provided' }); + const parts = token.split('.'); + if (parts.length < 2) return res.json({ success: false, error: 'Invalid JWT format' }); + const decodeB64 = s => JSON.parse(Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8')); + const header = decodeB64(parts[0]); + const payload = decodeB64(parts[1]); + let expired = null; + if (payload.exp) expired = Date.now() / 1000 > payload.exp; + res.json({ success: true, header, payload, expired, signature: parts[2] || null }); + } catch (err) { res.json({ success: false, error: 'Invalid JWT: ' + err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Timestamp Converter +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/timestamp', (req, res) => { + try { + const { value } = req.body; + let date; + if (!value || value === 'now') date = new Date(); + else if (/^\d{10}$/.test(value)) date = new Date(parseInt(value) * 1000); + else if (/^\d{13}$/.test(value)) date = new Date(parseInt(value)); + else date = new Date(value); + if (isNaN(date.getTime())) return res.json({ success: false, error: 'Invalid date/timestamp' }); + res.json({ + success: true, + unix: Math.floor(date.getTime() / 1000), + unixMs: date.getTime(), + iso: date.toISOString(), + utc: date.toUTCString(), + local: date.toString(), + relative: getRelativeTime(date) + }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +function getRelativeTime(date) { + const diff = (Date.now() - date.getTime()) / 1000; + const abs = Math.abs(diff); + const future = diff < 0; + const prefix = future ? 'in ' : ''; + const suffix = future ? '' : ' ago'; + if (abs < 60) return prefix + Math.round(abs) + 's' + suffix; + if (abs < 3600) return prefix + Math.round(abs/60) + 'm' + suffix; + if (abs < 86400) return prefix + Math.round(abs/3600) + 'h' + suffix; + if (abs < 2592000) return prefix + Math.round(abs/86400) + 'd' + suffix; + if (abs < 31536000) return prefix + Math.round(abs/2592000) + 'mo' + suffix; + return prefix + Math.round(abs/31536000) + 'y' + suffix; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: CSS Minifier +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/css/minify', (req, res) => { + try { + const { css } = req.body; + if (!css) return res.json({ success: false, error: 'No CSS provided' }); + const minified = css + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/\s+/g, ' ') + .replace(/\s*([{}:;,>~+])\s*/g, '$1') + .replace(/;}/g, '}') + .trim(); + const saved = css.length - minified.length; + const pct = css.length > 0 ? Math.round((saved / css.length) * 100) : 0; + res.json({ success: true, result: minified, original: css.length, minified: minified.length, saved, percentage: pct }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: HTML Entity Encode / Decode +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/html/encode', (req, res) => { + try { + const { text } = req.body; + const result = (text || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); + res.json({ success: true, result }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); +app.post('/api/html/decode', (req, res) => { + try { + const { text } = req.body; + const result = (text || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(///g, '/'); + res.json({ success: true, result }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: String Escape / Unescape +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/escape', (req, res) => { + try { + const { text } = req.body; + const result = JSON.stringify(text || ''); + res.json({ success: true, result: result.slice(1, -1) }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); +app.post('/api/unescape', (req, res) => { + try { + const { text } = req.body; + const result = JSON.parse('"' + (text || '') + '"'); + res.json({ success: true, result }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: IP Lookup +// ═══════════════════════════════════════════════════════════════════════════════ +app.get('/api/ip', async (req, res) => { + try { + const response = await fetch('http://ip-api.com/json/?fields=status,message,country,regionName,city,zip,lat,lon,timezone,isp,org,as,query'); + const data = await response.json(); + res.json({ success: true, ...data }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); +app.get('/api/ip/:ip', async (req, res) => { + try { + const response = await fetch(`http://ip-api.com/json/${req.params.ip}?fields=status,message,country,regionName,city,zip,lat,lon,timezone,isp,org,as,query`); + const data = await response.json(); + res.json({ success: true, ...data }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Text Statistics +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/text/stats', (req, res) => { + try { + const { text } = req.body; + const t = text || ''; + const words = t.trim() ? t.trim().split(/\s+/) : []; + const sentences = t.split(/[.!?]+/).filter(s => s.trim().length > 0); + const paragraphs = t.split(/\n\s*\n/).filter(p => p.trim().length > 0); + const readingTime = Math.max(1, Math.ceil(words.length / 200)); + const freq = {}; + for (const ch of t.toLowerCase()) { + if (/[a-z]/.test(ch)) freq[ch] = (freq[ch] || 0) + 1; + } + const topChars = Object.entries(freq).sort((a,b) => b[1]-a[1]).slice(0, 10); + res.json({ + success: true, + characters: t.length, + charactersNoSpaces: t.replace(/\s/g, '').length, + words: words.length, + sentences: sentences.length, + paragraphs: paragraphs.length, + lines: t.split('\n').length, + readingTime: readingTime + ' min', + topChars + }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Number Base Converter +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/number/convert', (req, res) => { + try { + const { value, fromBase } = req.body; + const base = parseInt(fromBase) || 10; + const num = parseInt(value, base); + if (isNaN(num)) return res.json({ success: false, error: 'Invalid number for the given base' }); + res.json({ + success: true, + decimal: num.toString(10), + binary: num.toString(2), + octal: num.toString(8), + hex: num.toString(16).toUpperCase() + }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Case Converter +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/text/case', (req, res) => { + try { + const { text } = req.body; + const t = text || ''; + res.json({ + success: true, + uppercase: t.toUpperCase(), + lowercase: t.toLowerCase(), + titleCase: t.replace(/\b\w/g, c => c.toUpperCase()), + camelCase: t.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase()), + snakeCase: t.toLowerCase().replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_|_$/g, ''), + kebabCase: t.toLowerCase().replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, ''), + dotCase: t.toLowerCase().replace(/[^a-zA-Z0-9]+/g, '.').replace(/^\.|\.$/g, ''), + reversed: t.split('').reverse().join('') + }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Cron Expression Parser +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/cron/parse', (req, res) => { + try { + const { expression } = req.body; + if (!expression) return res.json({ success: false, error: 'No cron expression provided' }); + const parts = expression.trim().split(/\s+/); + if (parts.length < 5 || parts.length > 6) return res.json({ success: false, error: 'Invalid cron expression. Expected 5 fields: minute hour day month weekday' }); + + const [minute, hour, dom, month, dow] = parts; + const fieldNames = { minute, hour, 'day of month': dom, month, 'day of week': dow }; + + // Build human-readable description + const desc = describeCron(minute, hour, dom, month, dow); + + // Calculate next 5 run times + const nextRuns = getNextCronRuns(parts, 5); + + res.json({ success: true, description: desc, fields: fieldNames, nextRuns }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +function describeCron(min, hr, dom, mon, dow) { + const segments = []; + + if (min === '*' && hr === '*') segments.push('Every minute'); + else if (min.startsWith('*/')) segments.push(`Every ${min.slice(2)} minutes`); + else if (hr.startsWith('*/')) { segments.push(`Every ${hr.slice(2)} hours`); if (min !== '0' && min !== '*') segments.push(`at minute ${min}`); } + else if (min !== '*' && hr !== '*') { + const h = parseInt(hr); + const ampm = h >= 12 ? 'PM' : 'AM'; + const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h; + segments.push(`At ${h12}:${min.padStart(2, '0')} ${ampm}`); + } else if (min !== '*') segments.push(`At minute ${min} of every hour`); + else if (hr !== '*') segments.push(`Every minute during hour ${hr}`); + + const dowNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; + const monNames = ['','January','February','March','April','May','June','July','August','September','October','November','December']; + + if (dom !== '*') segments.push(`on day ${dom} of the month`); + if (mon !== '*') { + const m = parseInt(mon); + segments.push(`in ${(m >= 1 && m <= 12) ? monNames[m] : 'month ' + mon}`); + } + if (dow !== '*') { + if (dow.includes('-')) { + const [s, e] = dow.split('-').map(Number); + segments.push(`on ${dowNames[s] || s} through ${dowNames[e] || e}`); + } else { + const days = dow.split(',').map(d => dowNames[parseInt(d)] || d).join(', '); + segments.push(`on ${days}`); + } + } + + return segments.join(', ') || 'Every minute'; +} + +function getNextCronRuns(parts, count) { + const [minExpr, hrExpr, domExpr, monExpr, dowExpr] = parts; + const runs = []; + const now = new Date(); + let check = new Date(now.getTime() + 60000); + check.setSeconds(0, 0); + + function matches(val, expr, max) { + if (expr === '*') return true; + if (expr.startsWith('*/')) return val % parseInt(expr.slice(2)) === 0; + if (expr.includes(',')) return expr.split(',').map(Number).includes(val); + if (expr.includes('-')) { const [s,e] = expr.split('-').map(Number); return val >= s && val <= e; } + return val === parseInt(expr); + } + + let iterations = 0; + while (runs.length < count && iterations < 525600) { + iterations++; + const m = check.getMinutes(), h = check.getHours(), d = check.getDate(), mo = check.getMonth() + 1, w = check.getDay(); + if (matches(m, minExpr) && matches(h, hrExpr) && matches(d, domExpr) && matches(mo, monExpr) && matches(w, dowExpr)) { + runs.push(check.toISOString().replace('T', ' ').slice(0, 16)); + } + check = new Date(check.getTime() + 60000); + } + return runs; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: JSON ↔ CSV Converter +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/convert/json-to-csv', (req, res) => { + try { + const { json } = req.body; + if (!json) return res.json({ success: false, error: 'No JSON provided' }); + const data = JSON.parse(json); + if (!Array.isArray(data) || data.length === 0) return res.json({ success: false, error: 'Input must be a non-empty JSON array of objects' }); + const headers = [...new Set(data.flatMap(obj => Object.keys(obj)))]; + const escapeCsv = v => { + const s = String(v === null || v === undefined ? '' : v); + return s.includes(',') || s.includes('"') || s.includes('\n') ? '"' + s.replace(/"/g, '""') + '"' : s; + }; + const rows = data.map(obj => headers.map(h => escapeCsv(obj[h])).join(',')); + const csv = [headers.join(','), ...rows].join('\n'); + res.json({ success: true, result: csv, rows: data.length, columns: headers.length }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +app.post('/api/convert/csv-to-json', (req, res) => { + try { + const { csv } = req.body; + if (!csv) return res.json({ success: false, error: 'No CSV provided' }); + const lines = csv.trim().split('\n'); + if (lines.length < 2) return res.json({ success: false, error: 'CSV must have a header row and at least one data row' }); + const headers = parseCsvLine(lines[0]); + const result = []; + for (let i = 1; i < lines.length; i++) { + if (!lines[i].trim()) continue; + const vals = parseCsvLine(lines[i]); + const obj = {}; + headers.forEach((h, idx) => { obj[h] = vals[idx] || ''; }); + result.push(obj); + } + res.json({ success: true, result }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +function parseCsvLine(line) { + const result = []; + let current = ''; + let inQuotes = false; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (inQuotes) { + if (ch === '"' && line[i + 1] === '"') { current += '"'; i++; } + else if (ch === '"') inQuotes = false; + else current += ch; + } else { + if (ch === '"') inQuotes = true; + else if (ch === ',') { result.push(current); current = ''; } + else current += ch; + } + } + result.push(current); + return result; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Text Encoder (ROT13, Binary, Morse, etc.) +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/text/encode', (req, res) => { + try { + const { text, method } = req.body; + if (!text) return res.json({ success: false, error: 'No text provided' }); + if (!method) return res.json({ success: false, error: 'No encoding method specified' }); + + let result; + switch (method) { + case 'rot13': + result = text.replace(/[a-zA-Z]/g, c => { + const base = c <= 'Z' ? 65 : 97; + return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base); + }); + break; + + case 'binary': + result = text.split('').map(c => c.charCodeAt(0).toString(2).padStart(8, '0')).join(' '); + break; + + case 'morse': { + const morseMap = { + 'A':'.-','B':'-...','C':'-.-.','D':'-..','E':'.','F':'..-.','G':'--.','H':'....', + 'I':'..','J':'.---','K':'-.-','L':'.-..','M':'--','N':'-.','O':'---','P':'.--.', + 'Q':'--.-','R':'.-.','S':'...','T':'-','U':'..-','V':'...-','W':'.--','X':'-..-', + 'Y':'-.--','Z':'--..','0':'-----','1':'.----','2':'..---','3':'...--','4':'....-', + '5':'.....','6':'-....','7':'--...','8':'---..','9':'----.', ' ':'/' + }; + result = text.toUpperCase().split('').map(c => morseMap[c] || c).join(' '); + break; + } + + case 'reverse': + result = text.split('').reverse().join(''); + break; + + case 'leetspeak': { + const leetMap = { 'A':'4','E':'3','G':'6','I':'1','O':'0','S':'5','T':'7','B':'8','L':'1' }; + result = text.split('').map(c => leetMap[c.toUpperCase()] || c).join(''); + break; + } + + case 'upside_down': { + const flipMap = { + 'a':'ɐ','b':'q','c':'ɔ','d':'p','e':'ǝ','f':'ɟ','g':'ƃ','h':'ɥ','i':'ᴉ','j':'ɾ', + 'k':'ʞ','l':'l','m':'ɯ','n':'u','o':'o','p':'d','q':'b','r':'ɹ','s':'s','t':'ʇ', + 'u':'n','v':'ʌ','w':'ʍ','x':'x','y':'ʎ','z':'z', + 'A':'∀','B':'q','C':'Ɔ','D':'p','E':'Ǝ','F':'Ⅎ','G':'פ','H':'H','I':'I','J':'ſ', + 'K':'ʞ','L':'˥','M':'W','N':'N','O':'O','P':'Ԁ','Q':'Q','R':'ɹ','S':'S','T':'⊥', + 'U':'∩','V':'Λ','W':'M','X':'X','Y':'⅄','Z':'Z', + '1':'Ɩ','2':'ᄅ','3':'Ɛ','4':'ㄣ','5':'ϛ','6':'9','7':'ㄥ','8':'8','9':'6','0':'0', + '.':'˙',',':'\'','?':'¿','!':'¡','\'':',','"':'„','(':')',')':'(','{':'}','}':'{', + '[':']',']':'[','<':'>','>':'<','&':'⅋','_':'‾' + }; + result = text.split('').map(c => flipMap[c] || c).reverse().join(''); + break; + } + + default: + return res.json({ success: false, error: `Unknown encoding method: ${method}` }); + } + + res.json({ success: true, result, method }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: HTTP Status Code Lookup +// ═══════════════════════════════════════════════════════════════════════════════ +const HTTP_STATUS_CODES = { + 100:'Continue',101:'Switching Protocols',102:'Processing',103:'Early Hints', + 200:'OK',201:'Created',202:'Accepted',203:'Non-Authoritative Information',204:'No Content', + 205:'Reset Content',206:'Partial Content',207:'Multi-Status',208:'Already Reported',226:'IM Used', + 300:'Multiple Choices',301:'Moved Permanently',302:'Found',303:'See Other',304:'Not Modified', + 307:'Temporary Redirect',308:'Permanent Redirect', + 400:'Bad Request',401:'Unauthorized',402:'Payment Required',403:'Forbidden',404:'Not Found', + 405:'Method Not Allowed',406:'Not Acceptable',407:'Proxy Authentication Required', + 408:'Request Timeout',409:'Conflict',410:'Gone',411:'Length Required',412:'Precondition Failed', + 413:'Payload Too Large',414:'URI Too Long',415:'Unsupported Media Type',416:'Range Not Satisfiable', + 418:"I'm a Teapot",422:'Unprocessable Entity',425:'Too Early',429:'Too Many Requests', + 451:'Unavailable For Legal Reasons', + 500:'Internal Server Error',501:'Not Implemented',502:'Bad Gateway',503:'Service Unavailable', + 504:'Gateway Timeout',505:'HTTP Version Not Supported',507:'Insufficient Storage', + 508:'Loop Detected',511:'Network Authentication Required' +}; + +app.get('/api/http-status', (req, res) => { + const entries = Object.entries(HTTP_STATUS_CODES).map(([code, text]) => ({ + code: parseInt(code), text, category: code[0] + 'xx' + })); + res.json({ success: true, statuses: entries }); +}); + +app.get('/api/http-status/:code', (req, res) => { + const code = req.params.code; + const text = HTTP_STATUS_CODES[code]; + if (!text) return res.json({ success: false, error: `Unknown status code: ${code}` }); + res.json({ success: true, code: parseInt(code), text, category: code[0] + 'xx' }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Placeholder Image Generator +// ═══════════════════════════════════════════════════════════════════════════════ +app.get('/api/placeholder/:width/:height', (req, res) => { + try { + const w = Math.min(Math.max(parseInt(req.params.width) || 400, 1), 2000); + const h = Math.min(Math.max(parseInt(req.params.height) || 300, 1), 2000); + const bg = (req.query.bg || '2a2a3a').replace(/[^0-9a-fA-F]/g, '').slice(0, 6); + const fg = (req.query.fg || '8888a0').replace(/[^0-9a-fA-F]/g, '').slice(0, 6); + const text = req.query.text || `${w}×${h}`; + const fontSize = Math.min(Math.max(parseInt(req.query.fontSize) || Math.min(w, h) / 8, 8), 200); + + // Generate SVG + const svg = ` + + + + ${text.replace(/&/g,'&').replace(//g,'>')} +`; + + res.setHeader('Content-Type', 'image/svg+xml'); + res.setHeader('Cache-Control', 'public, max-age=86400'); + res.send(svg); + } catch (err) { res.status(500).json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: SQL Formatter +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/sql/format', (req, res) => { + try { + const { sql } = req.body; + if (!sql) return res.json({ success: false, error: 'No SQL provided' }); + const majorKw = ['SELECT','FROM','WHERE','AND','OR','ORDER BY','GROUP BY','HAVING','LIMIT','OFFSET','INSERT INTO','VALUES','UPDATE','SET','DELETE FROM','CREATE TABLE','ALTER TABLE','DROP TABLE','JOIN','INNER JOIN','LEFT JOIN','RIGHT JOIN','FULL JOIN','CROSS JOIN','ON','UNION','UNION ALL','EXCEPT','INTERSECT','CASE','WHEN','THEN','ELSE','END','WITH','AS']; + let formatted = sql.replace(/\s+/g, ' ').trim(); + for (const kw of majorKw) { + const re = new RegExp('\\b' + kw.replace(/ /g, '\\s+') + '\\b', 'gi'); + formatted = formatted.replace(re, '\n' + kw.toUpperCase()); + } + // Indent non-keyword continuation lines + const lines = formatted.split('\n').filter(l => l.trim()); + const result = lines.map((line, i) => { + const trimmed = line.trim(); + if (i === 0) return trimmed; + const isKw = majorKw.some(kw => trimmed.toUpperCase().startsWith(kw)); + return isKw ? trimmed : ' ' + trimmed; + }).join('\n'); + res.json({ success: true, result }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +app.post('/api/sql/minify', (req, res) => { + try { + const { sql } = req.body; + if (!sql) return res.json({ success: false, error: 'No SQL provided' }); + const result = sql.replace(/\s+/g, ' ').replace(/\s*([,;()=<>!])\s*/g, '$1').trim(); + res.json({ success: true, result }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Byte Size Converter +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/bytes/convert', (req, res) => { + try { + const { value, unit, mode } = req.body; + if (value === undefined || value === null) return res.json({ success: false, error: 'No value provided' }); + const base = mode === 'si' ? 1000 : 1024; + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const idx = units.indexOf(unit || 'B'); + if (idx === -1) return res.json({ success: false, error: 'Invalid unit. Use B, KB, MB, GB, TB, or PB' }); + // Convert to bytes first + const bytes = parseFloat(value) * Math.pow(base, idx); + const result = {}; + for (let i = 0; i < units.length; i++) { + const val = bytes / Math.pow(base, i); + result[units[i]] = val >= 1 ? parseFloat(val.toPrecision(10)) : parseFloat(val.toExponential(4)); + } + res.json({ success: true, ...result }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: HMAC Generator +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/hmac', (req, res) => { + try { + const { message, secret, algorithm } = req.body; + if (!message) return res.json({ success: false, error: 'No message provided' }); + if (!secret) return res.json({ success: false, error: 'No secret key provided' }); + const algo = algorithm || 'sha256'; + const supported = ['sha256', 'sha512', 'sha1', 'md5']; + if (!supported.includes(algo)) return res.json({ success: false, error: `Unsupported algorithm. Use: ${supported.join(', ')}` }); + const hmac = crypto.createHmac(algo, secret).update(message, 'utf8').digest('hex'); + res.json({ success: true, hmac, algorithm: algo }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Slug Generator +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/text/slugify', (req, res) => { + try { + const { text, separator, lowercase } = req.body; + if (!text) return res.json({ success: false, error: 'No text provided' }); + const sep = separator || '-'; + let slug = text.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); // strip diacritics + if (lowercase !== false) slug = slug.toLowerCase(); + slug = slug + .replace(/[^a-zA-Z0-9\s-]/g, '') // remove special chars + .replace(/[\s-]+/g, sep) // replace spaces/hyphens with separator + .replace(new RegExp(`^\\${sep}+|\\${sep}+$`, 'g'), ''); // trim separator edges + res.json({ success: true, result: slug }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: Chmod Calculator +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/chmod/calculate', (req, res) => { + try { + const { numeric, symbolic } = req.body; + if (!numeric && !symbolic) return res.json({ success: false, error: 'Provide numeric or symbolic permissions' }); + + const digitToPerms = d => ({ read: !!(d & 4), write: !!(d & 2), execute: !!(d & 1) }); + const permsToDigit = (r, w, x) => (r ? 4 : 0) + (w ? 2 : 0) + (x ? 1 : 0); + const permsToStr = p => (p.read ? 'r' : '-') + (p.write ? 'w' : '-') + (p.execute ? 'x' : '-'); + + let owner, group, others; + + if (numeric) { + const digits = numeric.length === 4 ? numeric.slice(1) : numeric; + if (!/^[0-7]{3}$/.test(digits)) return res.json({ success: false, error: 'Invalid numeric permissions (use 3 octal digits, e.g. 755)' }); + owner = digitToPerms(parseInt(digits[0])); + group = digitToPerms(parseInt(digits[1])); + others = digitToPerms(parseInt(digits[2])); + } else { + const s = symbolic.replace(/^[-dlcbps]/, ''); // strip optional file type prefix + if (s.length < 9) return res.json({ success: false, error: 'Invalid symbolic permissions (need 9 characters, e.g. rwxr-xr-x)' }); + owner = { read: s[0] === 'r', write: s[1] === 'w', execute: s[2] === 'x' || s[2] === 's' }; + group = { read: s[3] === 'r', write: s[4] === 'w', execute: s[5] === 'x' || s[5] === 's' }; + others = { read: s[6] === 'r', write: s[7] === 'w', execute: s[8] === 'x' || s[8] === 't' }; + } + + const numStr = '' + permsToDigit(owner.read, owner.write, owner.execute) + + permsToDigit(group.read, group.write, group.execute) + + permsToDigit(others.read, others.write, others.execute); + const symStr = permsToStr(owner) + permsToStr(group) + permsToStr(others); + + res.json({ success: true, numeric: numStr, symbolic: symStr, owner, group, others }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: ASCII Art Generator +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/ascii/generate', (req, res) => { + try { + const { text } = req.body; + if (!text) return res.json({ success: false, error: 'No text provided' }); + const font = { + A:[' █ ','█ █','█████','█ █','█ █'],B:['████ ','█ █','████ ','█ █','████ '], + C:[' ████','█ ','█ ','█ ',' ████'],D:['████ ','█ █','█ █','█ █','████ '], + E:['█████','█ ','████ ','█ ','█████'],F:['█████','█ ','████ ','█ ','█ '], + G:[' ████','█ ','█ ██','█ █',' ████'],H:['█ █','█ █','█████','█ █','█ █'], + I:['█████',' █ ',' █ ',' █ ','█████'],J:['█████',' █',' █','█ █',' ███ '], + K:['█ █','█ █ ','███ ','█ █ ','█ █'],L:['█ ','█ ','█ ','█ ','█████'], + M:['█ █','██ ██','█ █ █','█ █','█ █'],N:['█ █','██ █','█ █ █','█ ██','█ █'], + O:[' ███ ','█ █','█ █','█ █',' ███ '],P:['████ ','█ █','████ ','█ ','█ '], + Q:[' ███ ','█ █','█ █ █','█ █ ',' ██ █'],R:['████ ','█ █','████ ','█ █ ','█ █'], + S:[' ████','█ ',' ███ ',' █','████ '],T:['█████',' █ ',' █ ',' █ ',' █ '], + U:['█ █','█ █','█ █','█ █',' ███ '],V:['█ █','█ █','█ █',' █ █ ',' █ '], + W:['█ █','█ █','█ █ █','██ ██','█ █'],X:['█ █',' █ █ ',' █ ',' █ █ ','█ █'], + Y:['█ █',' █ █ ',' █ ',' █ ',' █ '],Z:['█████',' █ ',' █ ',' █ ','█████'], + '0':[' ███ ','█ ██','█ █ █','██ █',' ███ '],'1':[' █ ',' ██ ',' █ ',' █ ','█████'], + '2':[' ███ ','█ █',' ██ ',' █ ','█████'],'3':['████ ',' █',' ███ ',' █','████ '], + '4':['█ █','█ █','█████',' █',' █'],'5':['█████','█ ','████ ',' █','████ '], + '6':[' ███ ','█ ','████ ','█ █',' ███ '],'7':['█████',' █ ',' █ ',' █ ',' █ '], + '8':[' ███ ','█ █',' ███ ','█ █',' ███ '],'9':[' ███ ','█ █',' ████',' █',' ███ '], + '!':[' █ ',' █ ',' █ ',' ',' █ '],'?':[' ███ ','█ █',' ██ ',' ',' █ '], + '.':[' ',' ',' ',' ',' █ '],'-':[' ',' ','█████',' ',' '], + ' ':[' ',' ',' ',' ',' '] + }; + const input = text.toUpperCase().slice(0, 30); + const lines = [[], [], [], [], []]; + for (const ch of input) { + const glyph = font[ch] || font[' ']; + for (let row = 0; row < 5; row++) { + lines[row].push(glyph[row]); + } + } + const result = lines.map(row => row.join(' ')).join('\n'); + res.json({ success: true, result }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// API: ENV ↔ JSON Converter +// ═══════════════════════════════════════════════════════════════════════════════ +app.post('/api/convert/env-to-json', (req, res) => { + try { + const { env } = req.body; + if (!env) return res.json({ success: false, error: 'No .env content provided' }); + const result = {}; + const lines = env.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; // skip empty lines and comments + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + let val = trimmed.slice(eqIdx + 1).trim(); + // strip surrounding quotes + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.slice(1, -1); + } + if (key) result[key] = val; + } + res.json({ success: true, result }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +app.post('/api/convert/json-to-env', (req, res) => { + try { + const { json } = req.body; + if (!json) return res.json({ success: false, error: 'No JSON provided' }); + const data = typeof json === 'string' ? JSON.parse(json) : json; + if (typeof data !== 'object' || Array.isArray(data)) return res.json({ success: false, error: 'JSON must be a flat object' }); + const lines = []; + for (const [key, val] of Object.entries(data)) { + const v = String(val); + const needsQuotes = v.includes(' ') || v.includes('=') || v.includes('#'); + lines.push(`${key}=${needsQuotes ? '"' + v + '"' : v}`); + } + res.json({ success: true, result: lines.join('\n'), count: lines.length }); + } catch (err) { res.json({ success: false, error: err.message }); } +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// SPA fallback +// ═══════════════════════════════════════════════════════════════════════════════ +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +// ─── Start ──────────────────────────────────────────────────────────────────── +app.listen(PORT, () => { + console.log(`\n ⚡ WinnieAPI-v2 running at http://localhost:${PORT}\n`); +});