Last active
September 24, 2025 10:35
-
-
Save rix1/f04d9034a4b58af7446ecf87c97c4e06 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>SMS Segment Visualizer</title> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/index.js"></script> | |
| <script src="https://cdn.jsdelivr.net/gh/TwilioDevEd/message-segment-calculator/docs/scripts/segmentsCalculator.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| </head> | |
| <body class="bg-gray-50 p-6 font-sans"> | |
| <div class="max-w-2xl mx-auto bg-white p-6 rounded-xl shadow"> | |
| <h1 class="text-2xl font-bold mb-4">SMS Segment Visualizer</h1> | |
| <p class="mb-2">Use this tool to craft clickable SMSes 💅</p> | |
| <p class="mb-4 italic text-gray-500"><strong>Why:</strong> Long messages are split into multiple segments. If a link/URL falls between segments, it might not be rendered as clickable on the customers phone!</p> | |
| <label for="message" class="block mb-2 font-medium">Type your SMS message</label> | |
| <textarea id="message" class="w-full p-2 border rounded-md mb-4" rows="5" placeholder="Paste your SMS here..."></textarea> | |
| <div id="info" class="mb-4"> | |
| <p><strong>Encoding:</strong> <span id="encodingUsed">-</span></p> | |
| <p><strong>Characters:</strong> <span id="charCount">0</span></p> | |
| <p><strong>Segments:</strong> <span id="segmentCount">0</span></p> | |
| </div> | |
| <div id="segments" class="space-y-2"></div> | |
| <div class="mt-6 p-4 bg-blue-50 rounded-lg"> | |
| <h2 class="font-semibold mb-2">Tips</h2> | |
| <ul class="list-disc list-inside space-y-1 text-sm text-gray-700"> | |
| <li>⚠️ Sakari will always include the protocol, <strong>http://</strong> in front of the URL, although this might not be rendered on the customer's phone!</li> | |
| <li>Using emojis or special characters (e.g. ☀️, €, —, " ") forces UCS-2 (shorter segments!).</li> | |
| <li>A link that spans a segment boundary may not be clickable on some devices.</li> | |
| <li>Each SMS has a limit of 160 characters (GSM-7) or 70 characters (UCS-2).</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <!-- Popover Modal --> | |
| <div id="clickable-modal" popover class="m-0 p-0 border-0 bg-transparent"> | |
| <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4"> | |
| <div class="bg-white rounded-lg p-6 max-w-sm w-full shadow-xl"> | |
| <div class="text-center"> | |
| <div class="text-4xl mb-4">✅</div> | |
| <h3 class="text-lg font-semibold text-green-600 mb-2">Success!</h3> | |
| <p class="text-gray-700 mb-4">I'm clickable! This link will work properly in SMS.</p> | |
| <button id="close-modal" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition-colors"> | |
| Close | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const msgBox = document.getElementById('message'); | |
| const encodingUsed = document.getElementById('encodingUsed'); | |
| const charCount = document.getElementById('charCount'); | |
| const segmentCount = document.getElementById('segmentCount'); | |
| const segmentsDiv = document.getElementById('segments'); | |
| const modal = document.getElementById('clickable-modal'); | |
| const closeModalBtn = document.getElementById('close-modal'); | |
| // Find all URLs and their character ranges | |
| function findUrlRanges(text) { | |
| const regex = /(https?:\/\/[^\s]+)/g; | |
| const ranges = []; | |
| let match; | |
| match = regex.exec(text); | |
| while (match !== null) { | |
| ranges.push({ start: match.index, end: match.index + match[0].length }); | |
| match = regex.exec(text); | |
| } | |
| return ranges; | |
| } | |
| const DOMAIN_LIST = ['http://sms.otovo.com', 'http://sa1.io', 'https://sms.otovo.com', 'https://sa1.io']; // add more as needed | |
| function escapeHtml(s) { | |
| return s.replace( | |
| /[&<>"']/g, | |
| (m) => | |
| ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ | |
| m | |
| ]) | |
| ); | |
| } | |
| // Final: pragmatic version—match until whitespace or one of < > " ' ) | |
| function domainRegex(domains = DOMAIN_LIST) { | |
| const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| const group = domains.map(esc).join('|'); | |
| return new RegExp( | |
| `\\b((?:https?:\\/\\/)?(?:www\\.)?(?:${group})\\/[^\\s<>"')]+)`, | |
| 'gi' | |
| ); | |
| } | |
| function highlightWhitelistedLinks(text, rx) { | |
| const matches = [...text.matchAll(rx)]; | |
| if (!matches.length) return escapeHtml(text); | |
| let out = ''; | |
| let last = 0; | |
| matches.forEach((m) => { | |
| const start = m.index; | |
| let end = start + m[0].length; | |
| while (end > start && /[.,!?;:)\]]$/.test(text.slice(start, end))) { | |
| end = end - 1; // trim trailing punctuation | |
| } | |
| out += escapeHtml(text.slice(last, start)); | |
| const url = text.slice(start, end); | |
| out += `<a href="#" class="text-blue-700 font-bold clickable-link" data-url="${escapeHtml(url)}">${escapeHtml(url)}</a>`; | |
| last = end; | |
| }); | |
| out += escapeHtml(text.slice(last)); | |
| return out; | |
| } | |
| // Handle link clicks to show modal | |
| function handleLinkClick(event) { | |
| if (event.target.classList.contains('clickable-link')) { | |
| event.preventDefault(); | |
| modal.showPopover(); | |
| } | |
| } | |
| // Close modal handlers | |
| closeModalBtn.addEventListener('click', () => { | |
| modal.hidePopover(); | |
| }); | |
| // Close modal when clicking outside (on backdrop) | |
| modal.addEventListener('click', (event) => { | |
| if (event.target === modal) { | |
| modal.hidePopover(); | |
| } | |
| }); | |
| // Close modal with Escape key | |
| document.addEventListener('keydown', (event) => { | |
| if (event.key === 'Escape' && modal.matches(':popover-open')) { | |
| modal.hidePopover(); | |
| } | |
| }); | |
| function update() { | |
| if (!msgBox || !encodingUsed || !charCount || !segmentCount || !segmentsDiv) | |
| return; | |
| const text = msgBox.value; | |
| const segmented = new SegmentedMessage(text, 'auto', true); | |
| encodingUsed.textContent = segmented.getEncodingName(); | |
| charCount.textContent = segmented.numberOfCharacters.toString(); | |
| segmentCount.textContent = segmented.segments.length.toString(); | |
| segmentsDiv.innerHTML = ''; | |
| segmented.segments.forEach((seg, idx) => { | |
| const color = idx % 2 === 0 ? 'bg-green-100' : 'bg-yellow-100'; | |
| const div = document.createElement('div'); | |
| div.className = `${color} p-2 rounded border`; | |
| const rawText = seg.map((e) => e.raw).join(''); | |
| const rx = domainRegex(DOMAIN_LIST); | |
| const highlighted = highlightWhitelistedLinks(rawText, rx); | |
| div.innerHTML = `<strong>Segment ${idx + 1}:</strong> ${highlighted}`; | |
| segmentsDiv.appendChild(div); | |
| }); | |
| } | |
| if (msgBox) { | |
| msgBox.addEventListener('input', update); | |
| } | |
| // Add event delegation for dynamically created links | |
| document.addEventListener('click', handleLinkClick); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment