|
| 1 | +<!doctype html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <!-- Code credits: One-numan (https://github.com/one-numan) --> |
| 5 | + <meta charset="utf-8" /> |
| 6 | + <title>Conventional Commit Builder (Single Page)</title> |
| 7 | + <meta name="viewport" content="width=device-width,initial-scale=1" /> |
| 8 | + <style> |
| 9 | + :root { |
| 10 | + --bg:#0f172a; --card:#111827; --border:#1f2937; --text:#e5e7eb; |
| 11 | + --muted:#9ca3af; --accent:#22c55e; --accent2:#60a5fa; --danger:#f97316; |
| 12 | + --ink:#0b1220; |
| 13 | + } |
| 14 | + * { box-sizing: border-box; } |
| 15 | + html, body { margin:0; padding:0; background:var(--bg); color:var(--text); font:14px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial, sans-serif; } |
| 16 | + .wrap { min-height:100vh; display:grid; grid-template-rows:auto 1fr auto; } |
| 17 | + header, main, footer { padding:18px; } |
| 18 | + header { background:var(--card); border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; } |
| 19 | + h1 { margin:0; font-size:20px; } |
| 20 | + .muted { color:var(--muted); } |
| 21 | + .btn, button, a.button { display:inline-block; text-decoration:none; background:var(--ink); border:1px solid var(--border); color:var(--text); padding:10px 12px; border-radius:8px; cursor:pointer; } |
| 22 | + .primary { background:var(--accent); color:#08130c; border-color:#16a34a; font-weight:700; } |
| 23 | + .secondary { background:var(--accent2); color:#071224; border-color:#3b82f6; font-weight:700; } |
| 24 | + .warn { background:var(--danger); color:#1b0e07; border-color:#ea580c; font-weight:700; } |
| 25 | + .grid { display:grid; grid-template-columns:1fr; gap:14px; max-width:980px; margin:18px auto; } |
| 26 | + @media (min-width: 840px) { .grid { grid-template-columns: 1fr 1fr; } } |
| 27 | + .card { background:var(--card); border:1px solid var(--border); border-radius:12px; padding:14px; } |
| 28 | + label { display:block; font-weight:600; margin:8px 0 6px; } |
| 29 | + input[type="text"], textarea, select { width:100%; background:var(--ink); color:var(--text); border:1px solid var(--border); border-radius:8px; padding:10px 12px; outline:none; } |
| 30 | + input[type="text"]:focus, textarea:focus, select:focus { border-color: var(--accent2); } |
| 31 | + textarea { min-height:110px; resize:vertical; } |
| 32 | + .row { display:grid; grid-template-columns:1fr 1fr; gap:10px; } |
| 33 | + .hint { color:var(--muted); font-size:12px; margin-top:6px; } |
| 34 | + .checkbox { display:flex; align-items:center; gap:10px; margin-top:10px; } |
| 35 | + pre, code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } |
| 36 | + pre { margin:0; padding:12px; background:var(--ink); border:1px solid var(--border); border-radius:8px; overflow:auto; white-space:pre-wrap; word-break:break-word; } |
| 37 | + .preview-header { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px; } |
| 38 | + .badge { font-size:12px; padding:2px 8px; background:var(--ink); border:1px solid var(--border); border-radius:999px; color:var(--muted); } |
| 39 | + .actions { display:flex; gap:10px; flex-wrap:wrap; margin-top:12px; } |
| 40 | + footer { background:var(--card); border-top:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; } |
| 41 | + .small { font-size:12px; color:var(--muted); } |
| 42 | + |
| 43 | + /* Modal */ |
| 44 | + .modal-backdrop { |
| 45 | + position: fixed; inset: 0; background: rgba(0,0,0,.6); |
| 46 | + display: none; align-items: center; justify-content: center; z-index: 50; |
| 47 | + } |
| 48 | + .modal { |
| 49 | + width: min(560px, 92vw); background: var(--card); border:1px solid var(--border); |
| 50 | + border-radius: 12px; padding: 16px; |
| 51 | + } |
| 52 | + .modal header { padding:0 0 10px 0; border:0; background:transparent; } |
| 53 | + .modal .close { float:right; } |
| 54 | + </style> |
| 55 | +</head> |
| 56 | +<body> |
| 57 | + <div class="wrap"> |
| 58 | + <header> |
| 59 | + <h1>Conventional Commit Builder</h1> |
| 60 | + <div class="actions"> |
| 61 | + <button id="creditsBtn" class="button">Credits</button> |
| 62 | + <a class="button" href="https://github.com/one-numan" target="_blank" rel="noopener noreferrer">GitHub</a> |
| 63 | + </div> |
| 64 | + </header> |
| 65 | + |
| 66 | + <main> |
| 67 | + <div class="grid"> |
| 68 | + <div class="card"> |
| 69 | + <div class="row"> |
| 70 | + <div> |
| 71 | + <label for="type">Type</label> |
| 72 | + <select id="type"> |
| 73 | + <option>feat</option><option>fix</option><option>docs</option> |
| 74 | + <option>style</option><option>refactor</option><option>perf</option> |
| 75 | + <option>test</option><option>build</option><option>ci</option> |
| 76 | + <option>chore</option><option>revert</option> |
| 77 | + </select> |
| 78 | + <div class="hint">Format: type(scope): subject</div> |
| 79 | + </div> |
| 80 | + <div> |
| 81 | + <label for="scope">Scope (optional)</label> |
| 82 | + <input id="scope" type="text" placeholder="e.g., auth, api, parser" /> |
| 83 | + <div class="hint">Example: feat(auth): add login</div> |
| 84 | + </div> |
| 85 | + </div> |
| 86 | + |
| 87 | + <label for="subject">Subject (short, imperative)</label> |
| 88 | + <input id="subject" type="text" placeholder="Add JWT-based login" /> |
| 89 | + <div class="hint">Keep concise and imperative.</div> |
| 90 | + |
| 91 | + <label for="body">Body (optional)</label> |
| 92 | + <textarea id="body" placeholder="Explain what and why, not how..."></textarea> |
| 93 | + |
| 94 | + <div class="checkbox"> |
| 95 | + <input id="breaking" type="checkbox" /> |
| 96 | + <label for="breaking" style="margin:0;">BREAKING CHANGE</label> |
| 97 | + </div> |
| 98 | + <input id="breakingDetails" type="text" placeholder="Describe the breaking change (footer)" /> |
| 99 | + |
| 100 | + <hr style="border-color:#1f2937; margin:14px 0;" /> |
| 101 | + |
| 102 | + <!-- Optional custom footers --> |
| 103 | + <div class="checkbox"> |
| 104 | + <input id="apiInclude" type="checkbox" /> |
| 105 | + <label for="apiInclude" style="margin:0;">API Endpoint (optional)</label> |
| 106 | + </div> |
| 107 | + <input id="apiEndpoint" type="text" placeholder="e.g., GET /v1/users/:id" /> |
| 108 | + |
| 109 | + <div class="checkbox" style="margin-top:12px;"> |
| 110 | + <input id="issueInclude" type="checkbox" /> |
| 111 | + <label for="issueInclude" style="margin:0;">Issue ID or Defect ID (optional)</label> |
| 112 | + </div> |
| 113 | + <div class="row"> |
| 114 | + <select id="issueType"> |
| 115 | + <option value="Issue-Id">Issue-Id</option> |
| 116 | + <option value="Defect-Id">Defect-Id</option> |
| 117 | + </select> |
| 118 | + <input id="issueNumber" type="text" placeholder="#123 or ABC-456" /> |
| 119 | + </div> |
| 120 | + |
| 121 | + <div class="checkbox" style="margin-top:12px;"> |
| 122 | + <input id="taskInclude" type="checkbox" /> |
| 123 | + <label for="taskInclude" style="margin:0;">Task Id and Assigned By (optional)</label> |
| 124 | + </div> |
| 125 | + <div class="row"> |
| 126 | + <input id="taskId" type="text" placeholder="Task-12345" /> |
| 127 | + <input id="assignedBy" type="text" placeholder="Assigned by: Name" /> |
| 128 | + </div> |
| 129 | + <div class="hint" style="margin-top:8px;">Footers: API-Endpoint, Issue-Id/Defect-Id, Task-Id, Assigned-By.</div> |
| 130 | + </div> |
| 131 | + |
| 132 | + <div class="card"> |
| 133 | + <div class="preview-header"> |
| 134 | + <strong>Preview</strong> |
| 135 | + <span class="badge" id="charCount">0 chars</span> |
| 136 | + </div> |
| 137 | + <pre id="preview">(message will appear here)</pre> |
| 138 | + |
| 139 | + <div class="actions"> |
| 140 | + <button class="primary" id="copyBtn">Copy commit</button> |
| 141 | + <button class="secondary" id="downloadBtn">Download .txt</button> |
| 142 | + <button class="warn" id="gitBtn">Show git commit command</button> |
| 143 | + </div> |
| 144 | + |
| 145 | + <div class="gitcmd" id="gitCmdWrap" style="display:none; margin-top:10px;"> |
| 146 | + <label>git commit command</label> |
| 147 | + <pre id="gitCmd"></pre> |
| 148 | + </div> |
| 149 | + </div> |
| 150 | + </div> |
| 151 | + </main> |
| 152 | + |
| 153 | + <footer> |
| 154 | + <div class="small">Based on Conventional Commits format.</div> |
| 155 | + <a class="small" href="https://www.conventionalcommits.org/en/v1.0.0/" target="_blank" rel="noopener noreferrer">Spec</a> |
| 156 | + </footer> |
| 157 | + </div> |
| 158 | + |
| 159 | + <!-- Credits modal --> |
| 160 | + <div id="creditsModal" class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="creditsTitle"> |
| 161 | + <div class="modal"> |
| 162 | + <header> |
| 163 | + <strong id="creditsTitle">Credits</strong> |
| 164 | + <button class="button close" id="creditsClose" aria-label="Close">Close</button> |
| 165 | + </header> |
| 166 | + <div> |
| 167 | + <p>Code credits go to <strong>One‑numan</strong> — <a href="https://github.com/one-numan" target="_blank" rel="noopener noreferrer">github.com/one-numan</a>.</p> |
| 168 | + <p class="muted">External links open in a new tab with rel="noopener noreferrer" for security and privacy.</p> |
| 169 | + </div> |
| 170 | + </div> |
| 171 | + </div> |
| 172 | + |
| 173 | + <script> |
| 174 | + const els = { |
| 175 | + type: document.getElementById('type'), |
| 176 | + scope: document.getElementById('scope'), |
| 177 | + subject: document.getElementById('subject'), |
| 178 | + body: document.getElementById('body'), |
| 179 | + breaking: document.getElementById('breaking'), |
| 180 | + breakingDetails: document.getElementById('breakingDetails'), |
| 181 | + apiInclude: document.getElementById('apiInclude'), |
| 182 | + apiEndpoint: document.getElementById('apiEndpoint'), |
| 183 | + issueInclude: document.getElementById('issueInclude'), |
| 184 | + issueType: document.getElementById('issueType'), |
| 185 | + issueNumber: document.getElementById('issueNumber'), |
| 186 | + taskInclude: document.getElementById('taskInclude'), |
| 187 | + taskId: document.getElementById('taskId'), |
| 188 | + assignedBy: document.getElementById('assignedBy'), |
| 189 | + preview: document.getElementById('preview'), |
| 190 | + charCount: document.getElementById('charCount'), |
| 191 | + copyBtn: document.getElementById('copyBtn'), |
| 192 | + downloadBtn: document.getElementById('downloadBtn'), |
| 193 | + gitBtn: document.getElementById('gitBtn'), |
| 194 | + gitCmd: document.getElementById('gitCmd'), |
| 195 | + gitCmdWrap: document.getElementById('gitCmdWrap'), |
| 196 | + creditsBtn: document.getElementById('creditsBtn'), |
| 197 | + creditsModal: document.getElementById('creditsModal'), |
| 198 | + creditsClose: document.getElementById('creditsClose'), |
| 199 | + }; |
| 200 | + |
| 201 | + function buildMessage() { |
| 202 | + const type = els.type.value.trim(); |
| 203 | + const scope = els.scope.value.trim(); |
| 204 | + const subject = els.subject.value.trim(); |
| 205 | + const body = els.body.value.replace(/\r\n/g, '\\n').trim(); |
| 206 | + const breaking = els.breaking.checked; |
| 207 | + const breakingDetails = els.breakingDetails.value.trim(); |
| 208 | + |
| 209 | + if (!type || !subject) return '(message will appear here)'; |
| 210 | + |
| 211 | + const hdrScope = scope ? `(${scope})` : ''; |
| 212 | + const bang = breaking ? '!' : ''; |
| 213 | + const header = `${type}${hdrScope}${bang}: ${subject}`; |
| 214 | + |
| 215 | + const parts = [header]; |
| 216 | + |
| 217 | + if (body) { |
| 218 | + parts.push(''); |
| 219 | + parts.push(body); |
| 220 | + } |
| 221 | + |
| 222 | + const footers = []; |
| 223 | + if (breaking && breakingDetails) { |
| 224 | + footers.push(`BREAKING CHANGE: ${breakingDetails}`); |
| 225 | + } |
| 226 | + if (els.apiInclude.checked && els.apiEndpoint.value.trim()) { |
| 227 | + footers.push(`API-Endpoint: ${els.apiEndpoint.value.trim()}`); |
| 228 | + } |
| 229 | + if (els.issueInclude.checked && els.issueNumber.value.trim()) { |
| 230 | + footers.push(`${els.issueType.value}: ${els.issueNumber.value.trim()}`); |
| 231 | + } |
| 232 | + if (els.taskInclude.checked) { |
| 233 | + const t = els.taskId.value.trim(); |
| 234 | + const a = els.assignedBy.value.trim(); |
| 235 | + if (t) footers.push(`Task-Id: ${t}`); |
| 236 | + if (a) footers.push(`Assigned-By: ${a}`); |
| 237 | + } |
| 238 | + if (footers.length) { |
| 239 | + if (!body) parts.push(''); |
| 240 | + parts.push(''); |
| 241 | + parts.push(...footers); |
| 242 | + } |
| 243 | + |
| 244 | + return parts.join('\\n'); |
| 245 | + } |
| 246 | + |
| 247 | + function update() { |
| 248 | + const msg = buildMessage(); |
| 249 | + els.preview.textContent = msg; |
| 250 | + els.charCount.textContent = `${msg.length} chars`; |
| 251 | + } |
| 252 | + |
| 253 | + function escapeForDoubleQuotes(str) { |
| 254 | + return str.replace(/\\\\/g, '\\\\\\\\').replace(/"/g, '\\"'); |
| 255 | + } |
| 256 | + |
| 257 | + function buildGitCommitCommand() { |
| 258 | + const msg = buildMessage(); |
| 259 | + const lines = msg.split('\\n'); |
| 260 | + const subject = lines || ''; |
| 261 | + const rest = lines.slice(1).join('\\n').trim(); |
| 262 | + |
| 263 | + const parts = []; |
| 264 | + parts.push(`git commit -m "${escapeForDoubleQuotes(subject)}"`); |
| 265 | + if (rest) parts.push(`-m "${escapeForDoubleQuotes(rest)}"`); |
| 266 | + return parts.join(' '); |
| 267 | + } |
| 268 | + |
| 269 | + ['change','keyup','input'].forEach(evt => { |
| 270 | + [ |
| 271 | + 'type','scope','subject','body','breaking','breakingDetails', |
| 272 | + 'apiInclude','apiEndpoint','issueInclude','issueType','issueNumber', |
| 273 | + 'taskInclude','taskId','assignedBy' |
| 274 | + ].forEach(id => document.getElementById(id).addEventListener(evt, update)); |
| 275 | + }); |
| 276 | + update(); |
| 277 | + |
| 278 | + // Clipboard copy (secure context recommended) |
| 279 | + els.copyBtn.addEventListener('click', async () => { |
| 280 | + const msg = buildMessage(); |
| 281 | + try { |
| 282 | + await navigator.clipboard.writeText(msg); |
| 283 | + els.copyBtn.textContent = 'Copied!'; |
| 284 | + setTimeout(() => (els.copyBtn.textContent = 'Copy commit'), 1200); |
| 285 | + } catch (e) { |
| 286 | + alert('Clipboard copy failed. Select and copy manually.'); |
| 287 | + } |
| 288 | + }); |
| 289 | + |
| 290 | + // Download as .txt |
| 291 | + els.downloadBtn.addEventListener('click', () => { |
| 292 | + const msg = buildMessage(); |
| 293 | + const blob = new Blob([msg], { type: 'text/plain;charset=utf-8' }); |
| 294 | + const url = URL.createObjectURL(blob); |
| 295 | + const a = document.createElement('a'); |
| 296 | + a.href = url; a.download = 'commit-message.txt'; |
| 297 | + document.body.appendChild(a); a.click(); a.remove(); |
| 298 | + URL.revokeObjectURL(url); |
| 299 | + }); |
| 300 | + |
| 301 | + // Show git commit command |
| 302 | + els.gitBtn.addEventListener('click', () => { |
| 303 | + els.gitCmdWrap.style.display = 'block'; |
| 304 | + els.gitCmd.textContent = buildGitCommitCommand(); |
| 305 | + }); |
| 306 | + |
| 307 | + // Credits modal |
| 308 | + function openCredits() { els.creditsModal.style.display = 'flex'; } |
| 309 | + function closeCredits() { els.creditsModal.style.display = 'none'; } |
| 310 | + els.creditsBtn.addEventListener('click', openCredits); |
| 311 | + els.creditsClose.addEventListener('click', closeCredits); |
| 312 | + els.creditsModal.addEventListener('click', (e) => { if (e.target === els.creditsModal) closeCredits(); }); |
| 313 | + window.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeCredits(); }); |
| 314 | + </script> |
| 315 | +</body> |
| 316 | +</html> |
0 commit comments