Skip to content

Instantly share code, notes, and snippets.

@rix1
Last active September 24, 2025 10:35
Show Gist options
  • Select an option

  • Save rix1/f04d9034a4b58af7446ecf87c97c4e06 to your computer and use it in GitHub Desktop.

Select an option

Save rix1/f04d9034a4b58af7446ecf87c97c4e06 to your computer and use it in GitHub Desktop.
<!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) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[
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