Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save krazy-glue/776f8b63bf9c6d2334fb2d4fa6c7b5ca to your computer and use it in GitHub Desktop.

Select an option

Save krazy-glue/776f8b63bf9c6d2334fb2d4fa6c7b5ca to your computer and use it in GitHub Desktop.
Local Subscription Visualizer
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Subscription Tracker</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 350px 1fr;
gap: 30px;
align-items: start;
}
.input-section {
background: white;
padding: 30px;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 24px;
display: flex;
align-items: center;
gap: 10px;
}
h1 .emoji {
font-size: 32px;
}
.subtitle {
color: #666;
margin-bottom: 25px;
font-size: 13px;
}
.subscription-form {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-size: 13px;
font-weight: 500;
}
input, select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
.color-input {
display: flex;
gap: 10px;
}
input[type="color"] {
width: 60px;
height: 45px;
border: none;
cursor: pointer;
}
button {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.subscription-list {
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
}
.subscription-item {
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.subscription-info {
flex: 1;
}
.subscription-name {
font-weight: 600;
color: #333;
font-size: 14px;
}
.subscription-price {
color: #667eea;
font-weight: 700;
font-size: 14px;
}
.delete-btn {
width: auto;
padding: 6px 12px;
background: #ff4757;
font-size: 12px;
}
.delete-btn:hover {
background: #ff3838;
}
.visualization-section {
background: white;
padding: 30px;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
min-height: 600px;
}
.treemap-container {
width: 100%;
min-height: 500px;
height: auto;
position: relative;
margin-bottom: 30px;
background: #f8f9fa;
border-radius: 15px;
overflow: visible;
}
.treemap-box {
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px;
border-radius: 15px;
color: #333;
transition: all 0.3s;
cursor: pointer;
overflow: hidden;
}
.treemap-box:hover {
transform: scale(1.02);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
z-index: 10;
}
.box-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.box-icon {
font-size: 40px;
line-height: 1;
}
.box-percentage {
background: rgba(255,255,255,0.9);
padding: 6px 12px;
border-radius: 20px;
font-weight: 700;
font-size: 13px;
}
.box-content {
margin-top: auto;
}
.box-name {
font-weight: 700;
font-size: 16px;
margin-bottom: 5px;
}
.box-price {
font-size: 32px;
font-weight: 800;
margin-bottom: 2px;
}
.box-yearly {
font-size: 14px;
opacity: 0.7;
}
.totals {
display: flex;
justify-content: space-between;
gap: 20px;
}
.total-box {
flex: 1;
padding: 25px;
border-radius: 15px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.total-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
color: #999;
font-weight: 600;
margin-bottom: 8px;
}
.total-amount {
font-size: 40px;
font-weight: 800;
color: #333;
}
.yearly-amount {
color: #667eea;
}
@media (max-width: 1024px) {
.container {
grid-template-columns: 1fr;
}
}
.emoji-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 1000;
justify-content: center;
align-items: center;
}
.emoji-modal-content {
background: white;
padding: 30px;
border-radius: 20px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.emoji-modal h2 {
margin-bottom: 20px;
color: #333;
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.emoji-option {
font-size: 32px;
padding: 10px;
text-align: center;
cursor: pointer;
border-radius: 10px;
transition: all 0.2s;
background: #f8f9fa;
}
.emoji-option:hover {
background: #667eea;
transform: scale(1.1);
}
.close-modal {
width: 100%;
padding: 12px;
background: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="input-section">
<h1><span class="emoji">๐Ÿ’ฐ</span>Subscription Tracker</h1>
<p class="subtitle">Track and visualize your monthly expenses</p>
<form class="subscription-form" id="subscriptionForm">
<div class="form-group">
<label for="name">Subscription Name</label>
<input type="text" id="name" placeholder="e.g., Netflix" required>
</div>
<div class="form-group">
<label for="price">Monthly Cost ($)</label>
<input type="number" id="price" placeholder="0.00" step="0.01" min="0" required>
</div>
<div class="form-group">
<label for="icon">Icon (emoji)</label>
<div style="display: flex; gap: 8px;">
<input type="text" id="icon" placeholder="๐ŸŽฌ" maxlength="2" style="flex: 1;">
<button type="button" id="emojiSearchBtn" style="width: auto; padding: 12px 20px; background: #4CAF50;">
๐Ÿ”
</button>
</div>
</div>
<div class="form-group">
<label for="color">Color</label>
<div class="color-input">
<input type="color" id="color" value="#ff6b9d">
<input type="text" id="colorHex" value="#ff6b9d" readonly style="flex: 1;">
</div>
</div>
<button type="submit">Add Subscription</button>
<div style="display: flex; gap: 8px; margin-top: 15px;">
<button type="button" id="saveDataBtn" style="background: #28a745; font-size: 13px; padding: 10px;">
๐Ÿ’พ Save Data
</button>
<button type="button" id="loadDataBtn" style="background: #17a2b8; font-size: 13px; padding: 10px;">
๐Ÿ“‚ Load Data
</button>
</div>
<div style="margin-top: 10px;">
<button type="button" id="saveImageBtn" style="background: #ff6b6b; font-size: 13px; padding: 10px;">
๐Ÿ“ธ Save as Image
</button>
</div>
<input type="file" id="fileInput" accept=".json" style="display: none;">
</form>
<div class="subscription-list" id="subscriptionList"></div>
</div>
<div class="visualization-section">
<div class="treemap-container" id="treemap"></div>
<div class="totals">
<div class="total-box">
<div class="total-label">Total / Month</div>
<div class="total-amount" id="monthlyTotal">$0.00</div>
</div>
<div class="total-box">
<div class="total-label">Yearly Projection</div>
<div class="total-amount yearly-amount" id="yearlyTotal">$0.00</div>
</div>
</div>
</div>
</div>
<script>
let subscriptions = [];
const colorInput = document.getElementById('color');
const colorHex = document.getElementById('colorHex');
colorInput.addEventListener('input', (e) => {
colorHex.value = e.target.value;
});
document.getElementById('subscriptionForm').addEventListener('submit', (e) => {
e.preventDefault();
const name = document.getElementById('name').value;
const price = parseFloat(document.getElementById('price').value);
const icon = document.getElementById('icon').value || '๐Ÿ“ฆ';
const color = document.getElementById('color').value;
subscriptions.push({ name, price, icon, color });
updateVisualization();
updateList();
e.target.reset();
document.getElementById('color').value = '#' + Math.floor(Math.random()*16777215).toString(16);
colorHex.value = document.getElementById('color').value;
});
function deleteSubscription(index) {
subscriptions.splice(index, 1);
updateVisualization();
updateList();
}
function updateList() {
const listContainer = document.getElementById('subscriptionList');
listContainer.innerHTML = '';
subscriptions.forEach((sub, index) => {
const item = document.createElement('div');
item.className = 'subscription-item';
item.innerHTML = `
<div class="subscription-info">
<div class="subscription-name">${sub.icon} ${sub.name}</div>
<div class="subscription-price">$${sub.price.toFixed(2)}/mo</div>
</div>
<button class="delete-btn" onclick="deleteSubscription(${index})">Delete</button>
`;
listContainer.appendChild(item);
});
}
function updateVisualization() {
const treemapContainer = document.getElementById('treemap');
const monthlyTotalEl = document.getElementById('monthlyTotal');
const yearlyTotalEl = document.getElementById('yearlyTotal');
const total = subscriptions.reduce((sum, sub) => sum + sub.price, 0);
monthlyTotalEl.textContent = `$${total.toFixed(2)}`;
yearlyTotalEl.textContent = `$${(total * 12).toFixed(2)}`;
if (subscriptions.length === 0) {
treemapContainer.innerHTML = `
<div style="padding: 40px; color: #5a67d8; font-size: 15px; line-height: 1.8;">
<p style="font-size: 18px; font-weight: 600; margin-bottom: 20px; color: #4c51bf;">Add subscriptions to see visualization and totals</p>
<p style="margin-bottom: 15px; color: #555;">Consider tracking different categories separately for better insights. Use the <strong>๐Ÿ’พ Save Data</strong> button to save each category, then start fresh for the next one:</p>
<ul style="margin-left: 20px; margin-bottom: 20px;">
<li><strong>Video Streaming:</strong> Netflix, Hulu, Disney+, HBO Max, Apple TV+, YouTube Premium, Amazon Prime Video, Paramount+, Peacock</li>
<li><strong>Music & Podcasts:</strong> Spotify, Apple Music, YouTube Music, Tidal, Amazon Music, Audible, podcast subscriptions</li>
<li><strong>Fitness & Wellness:</strong> Gym membership, yoga studio, Peloton, fitness apps, meditation apps, wellness coaching</li>
<li><strong>Gaming:</strong> Xbox Game Pass, PlayStation Plus, Nintendo Online, Steam, Epic Games, gaming subscriptions</li>
<li><strong>Productivity & Work:</strong> Microsoft 365, Google Workspace, Adobe Creative Cloud, Dropbox, Notion, Slack, Zoom</li>
<li><strong>Food & Dining:</strong> Meal delivery services, DoorDash Pass, Uber Eats, restaurant memberships, coffee subscriptions</li>
<li><strong>Transportation:</strong> Uber/Lyft subscriptions, public transit passes, parking, car washes, toll passes</li>
<li><strong>News & Media:</strong> New York Times, Washington Post, Medium, Substack subscriptions, magazine subscriptions</li>
<li><strong>Shopping & Retail:</strong> Amazon Prime, Costco membership, subscription boxes, beauty boxes, clothing rentals</li>
<li><strong>Education & Learning:</strong> Online courses, language learning apps, skill development platforms, audiobook services</li>
<li><strong>Cloud Storage & Backup:</strong> iCloud, Google Drive, Dropbox, OneDrive, backup services</li>
<li><strong>Security & Privacy:</strong> VPN services, password managers, antivirus software, identity protection</li>
</ul>
<p style="font-style: italic; color: #666; font-size: 14px; margin-top: 15px;">๐Ÿ’ก <strong>Pro tip:</strong> Keep each visualization to 8-12 items for the best display. For comprehensive expense tracking with many subscriptions, save separate categories and review them individually!</p>
</div>
`;
return;
}
treemapContainer.innerHTML = '';
const sortedSubs = [...subscriptions].sort((a, b) => b.price - a.price);
const containerWidth = treemapContainer.offsetWidth;
const containerHeight = Math.max(500, treemapContainer.offsetHeight);
const boxes = sortedSubs.map(sub => ({
...sub,
value: sub.price,
percentage: (sub.price / total * 100).toFixed(0)
}));
layoutTreemap(boxes, 0, 0, containerWidth, containerHeight, treemapContainer);
}
function layoutTreemap(items, x, y, width, height, container) {
if (items.length === 0) return;
const gap = 10;
const availableWidth = width - gap;
const availableHeight = height - gap;
layoutSimpleTreemap(items, x + gap/2, y + gap/2, availableWidth, availableHeight, gap, container);
}
function layoutSimpleTreemap(items, x, y, width, height, gap, container) {
if (items.length === 0) return;
const total = items.reduce((sum, item) => sum + item.value, 0);
if (items.length === 1) {
createBox(items[0], x, y, width - gap, height - gap, container);
return;
}
const totalArea = width * height;
const avgArea = totalArea / items.length;
const idealBoxSize = Math.sqrt(avgArea);
const cols = Math.max(1, Math.floor(width / idealBoxSize));
const rows = Math.ceil(items.length / cols);
const rowHeight = (height - (rows - 1) * gap) / rows;
const neededHeight = rows * rowHeight + (rows - 1) * gap;
if (neededHeight > height) {
container.style.height = `${neededHeight + gap}px`;
}
let currentIndex = 0;
for (let row = 0; row < rows; row++) {
const itemsInRow = Math.min(cols, items.length - currentIndex);
const rowItems = items.slice(currentIndex, currentIndex + itemsInRow);
const rowTotal = rowItems.reduce((sum, it) => sum + it.value, 0);
let currentX = x;
const currentY = y + row * (rowHeight + gap);
rowItems.forEach((item, colIndex) => {
const boxWidth = ((width - (itemsInRow - 1) * gap) * (item.value / rowTotal)) - gap;
const boxHeight = rowHeight - gap;
createBox(item, currentX, currentY, boxWidth, boxHeight, container);
currentX += boxWidth + gap;
});
currentIndex += itemsInRow;
}
}
function createBox(item, x, y, width, height, container) {
const box = document.createElement('div');
box.className = 'treemap-box';
box.style.left = `${x}px`;
box.style.top = `${y}px`;
box.style.width = `${width}px`;
box.style.height = `${height}px`;
const baseColor = item.color;
box.style.background = `
linear-gradient(135deg,
${lightenColor(baseColor, 30)} 0%,
${baseColor} 50%,
${darkenColor(baseColor, 20)} 100%)
`;
const minDimension = Math.min(width, height);
const iconSize = Math.max(24, Math.min(60, minDimension * 0.2));
const nameSize = Math.max(12, Math.min(18, minDimension * 0.08));
const priceSize = Math.max(18, Math.min(36, minDimension * 0.15));
box.innerHTML = `
<div class="box-header">
<div class="box-icon" style="font-size: ${iconSize}px;">${item.icon}</div>
<div class="box-percentage">${item.percentage}%</div>
</div>
<div class="box-content">
<div class="box-name" style="font-size: ${nameSize}px;">${item.name}</div>
<div class="box-price" style="font-size: ${priceSize}px;">$${item.price.toFixed(2)}</div>
<div class="box-yearly">~$${(item.price * 12).toFixed(2)}/yr</div>
</div>
`;
container.appendChild(box);
}
function lightenColor(color, percent) {
const num = parseInt(color.replace("#",""), 16);
const amt = Math.round(2.55 * percent);
const R = Math.min(255, (num >> 16) + amt);
const G = Math.min(255, (num >> 8 & 0x00FF) + amt);
const B = Math.min(255, (num & 0x0000FF) + amt);
return "#" + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
}
function darkenColor(color, percent) {
const num = parseInt(color.replace("#",""), 16);
const amt = Math.round(2.55 * percent);
const R = Math.max(0, (num >> 16) - amt);
const G = Math.max(0, (num >> 8 & 0x00FF) - amt);
const B = Math.max(0, (num & 0x0000FF) - amt);
return "#" + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
}
document.getElementById('saveDataBtn').addEventListener('click', () => {
const data = JSON.stringify(subscriptions, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `subscriptions_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
});
const fileInput = document.getElementById('fileInput');
document.getElementById('loadDataBtn').addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
try {
const data = JSON.parse(event.target.result);
subscriptions = data;
updateVisualization();
updateList();
} catch (error) {
alert('Error loading file. Please make sure it\'s a valid subscription data file.');
}
};
reader.readAsText(file);
}
fileInput.value = '';
});
document.getElementById('saveImageBtn').addEventListener('click', async () => {
const treemapContainer = document.getElementById('treemap');
if (subscriptions.length === 0) {
alert('Add some subscriptions first!');
return;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const rect = treemapContainer.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height + 120;
ctx.fillStyle = '#f8f9fa';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const boxes = treemapContainer.querySelectorAll('.treemap-box');
boxes.forEach((box, index) => {
const boxRect = box.getBoundingClientRect();
const containerRect = treemapContainer.getBoundingClientRect();
const x = boxRect.left - containerRect.left;
const y = boxRect.top - containerRect.top;
const width = boxRect.width;
const height = boxRect.height;
const sub = subscriptions.find(s => s.name === box.querySelector('.box-name').textContent);
const baseColor = sub ? sub.color : '#cccccc';
ctx.fillStyle = baseColor;
ctx.fillRect(x, y, width, height);
ctx.strokeStyle = baseColor;
ctx.lineWidth = 1;
ctx.strokeRect(x, y, width, height);
const icon = box.querySelector('.box-icon').textContent;
const percentage = box.querySelector('.box-percentage').textContent;
const name = box.querySelector('.box-name').textContent;
const price = box.querySelector('.box-price').textContent;
const yearly = box.querySelector('.box-yearly').textContent;
ctx.fillStyle = '#333';
ctx.font = `${Math.max(24, Math.min(60, height * 0.2))}px Arial`;
ctx.fillText(icon, x + 20, y + 50);
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.font = 'bold 13px Arial';
const percentageWidth = ctx.measureText(percentage).width + 24;
ctx.fillRect(x + width - percentageWidth - 10, y + 10, percentageWidth, 30);
ctx.fillStyle = '#333';
ctx.fillText(percentage, x + width - percentageWidth + 2, y + 30);
ctx.fillStyle = '#333';
ctx.font = `bold ${Math.max(12, Math.min(18, height * 0.08))}px Arial`;
ctx.fillText(name, x + 20, y + height - 70);
ctx.font = `bold ${Math.max(18, Math.min(36, height * 0.15))}px Arial`;
ctx.fillText(price, x + 20, y + height - 35);
ctx.fillStyle = 'rgba(51, 51, 51, 0.7)';
ctx.font = `${Math.max(12, Math.min(14, height * 0.07))}px Arial`;
ctx.fillText(yearly, x + 20, y + height - 15);
});
const monthlyTotal = subscriptions.reduce((sum, sub) => sum + sub.price, 0);
const yearlyTotal = monthlyTotal * 12;
const totalBoxY = rect.height;
ctx.fillStyle = '#e9ecef';
const box1Width = canvas.width / 2 - 20;
ctx.fillRect(10, totalBoxY + 10, box1Width, 100);
ctx.fillRect(canvas.width / 2 + 10, totalBoxY + 10, box1Width, 100);
ctx.fillStyle = '#999';
ctx.font = 'bold 11px Arial';
ctx.fillText('TOTAL / MONTH', 30, totalBoxY + 35);
ctx.fillText('YEARLY PROJECTION', canvas.width / 2 + 30, totalBoxY + 35);
ctx.fillStyle = '#333';
ctx.font = 'bold 36px Arial';
ctx.fillText(`$${monthlyTotal.toFixed(2)}`, 30, totalBoxY + 75);
ctx.fillStyle = '#667eea';
ctx.fillText(`$${yearlyTotal.toFixed(2)}`, canvas.width / 2 + 30, totalBoxY + 75);
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `subscription-tracker_${new Date().toISOString().split('T')[0]}.png`;
a.click();
URL.revokeObjectURL(url);
});
});
const emojiSearchBtn = document.getElementById('emojiSearchBtn');
const nameInput = document.getElementById('name');
const iconInput = document.getElementById('icon');
const modal = document.createElement('div');
modal.className = 'emoji-modal';
modal.innerHTML = `
<div class="emoji-modal-content">
<h2>Select an Emoji</h2>
<input type="text" id="emojiSearchInput" placeholder="Search emojis..." style="width: 100%; margin-bottom: 15px; padding: 12px; border: 2px solid #e0e0e0; border-radius: 10px;">
<div id="emojiGrid" class="emoji-grid"></div>
<div style="margin-top: 15px; text-align: center;">
<a id="emojiDbLink" href="#" target="_blank" style="color: #667eea; text-decoration: none; font-size: 14px;">๐Ÿ”— Search more on EmojiDB</a>
</div>
<button class="close-modal" onclick="this.closest('.emoji-modal').style.display='none'">Close</button>
</div>
`;
document.body.appendChild(modal);
const emojiDatabase = {
'entertainment': ['๐Ÿ“บ', '๐ŸŽฌ', '๐ŸŽฎ', '๐ŸŽต', '๐ŸŽง', '๐Ÿ“ป', '๐ŸŽญ', '๐ŸŽช', '๐ŸŽจ', '๐ŸŽค', '๐ŸŽธ', '๐ŸŽน', '๐ŸŽบ', '๐ŸŽท', '๐Ÿฅ'],
'streaming': ['๐Ÿ“บ', '๐ŸŽฌ', '๐ŸŽฅ', '๐Ÿ“น', '๐ŸŽž๏ธ', '๐Ÿ“ฝ๏ธ', '๐Ÿฟ', '๐ŸŽญ'],
'music': ['๐ŸŽต', '๐ŸŽถ', '๐ŸŽง', '๐ŸŽค', '๐ŸŽธ', '๐ŸŽน', '๐ŸŽบ', '๐ŸŽท', '๐Ÿฅ', '๐ŸŽผ', '๐Ÿ“ป'],
'fitness': ['๐Ÿ’ช', '๐Ÿ‹๏ธ', '๐Ÿคธ', '๐Ÿง˜', '๐Ÿƒ', '๐Ÿšด', 'โ›น๏ธ', '๐Ÿคพ', '๐ŸŠ', '๐Ÿง—', 'โšฝ', '๐Ÿ€', '๐ŸŽพ', '๐Ÿ', '๐ŸฅŠ'],
'food': ['๐Ÿ•', '๐Ÿ”', '๐ŸŸ', '๐ŸŒฎ', '๐ŸŒฏ', '๐Ÿฅ—', '๐Ÿ', '๐Ÿœ', '๐Ÿฑ', '๐Ÿ›', '๐Ÿฒ', '๐Ÿฅ˜', '๐Ÿณ', '๐Ÿฅž', 'โ˜•', '๐Ÿต'],
'technology': ['๐Ÿ’ป', '๐Ÿ–ฅ๏ธ', 'โŒจ๏ธ', '๐Ÿ–ฑ๏ธ', '๐Ÿ–จ๏ธ', '๐Ÿ“ฑ', 'โ˜๏ธ', '๐ŸŒ', '๐Ÿ“ก', '๐Ÿ”Œ', '๐Ÿ”‹', '๐Ÿ’พ', '๐Ÿ’ฟ', '๐Ÿ“€'],
'business': ['๐Ÿ’ผ', '๐Ÿ“Š', '๐Ÿ“ˆ', '๐Ÿ“‰', '๐Ÿ’ฐ', '๐Ÿ’ต', '๐Ÿ’ณ', '๐Ÿข', '๐Ÿฆ', '๐Ÿ“', '๐Ÿ“‹', '๐Ÿ“Œ', '๐Ÿ“Ž', '๐Ÿ–Š๏ธ'],
'education': ['๐Ÿ“š', '๐Ÿ“–', '๐Ÿ“', 'โœ๏ธ', '๐Ÿ““', '๐Ÿ“”', '๐Ÿ“•', '๐Ÿ“—', '๐Ÿ“˜', '๐Ÿ“™', '๐ŸŽ“', '๐Ÿซ', '๐Ÿ–Š๏ธ', 'โœ’๏ธ'],
'communication': ['๐Ÿ“ง', '๐Ÿ“จ', '๐Ÿ“ฉ', '๐Ÿ’Œ', '๐Ÿ“ฎ', '๐Ÿ“ฌ', '๐Ÿ“ญ', '๐Ÿ“ช', '๐Ÿ“ซ', '๐Ÿ’ฌ', '๐Ÿ’ญ', '๐Ÿ—จ๏ธ', '๐Ÿ—ฏ๏ธ', '๐Ÿ“ž', 'โ˜Ž๏ธ'],
'shopping': ['๐Ÿ›’', '๐Ÿ›๏ธ', '๐Ÿ’ณ', '๐Ÿ’ฐ', '๐Ÿ’ต', '๐Ÿ’ด', '๐Ÿ’ถ', '๐Ÿ’ท', '๐Ÿช', '๐Ÿฌ', '๐ŸŽ', '๐ŸŽ€'],
'travel': ['โœˆ๏ธ', '๐Ÿš—', '๐Ÿš•', '๐Ÿš™', '๐ŸšŒ', '๐ŸšŽ', '๐ŸŽ๏ธ', '๐Ÿš“', '๐Ÿš‘', '๐Ÿš’', '๐Ÿš', '๐Ÿ›ด', '๐Ÿšฒ', '๐Ÿ›ต', '๐Ÿ๏ธ', '๐Ÿ—บ๏ธ', '๐Ÿงณ'],
'tools': ['๐Ÿ”ง', '๐Ÿ”จ', 'โš’๏ธ', '๐Ÿ› ๏ธ', 'โ›๏ธ', '๐Ÿ”ฉ', 'โš™๏ธ', '๐Ÿ—œ๏ธ', 'โš—๏ธ', '๐Ÿ”ฌ', '๐Ÿ”ญ', '๐Ÿ“ก', '๐Ÿงฐ'],
'security': ['๐Ÿ”’', '๐Ÿ”“', '๐Ÿ”', '๐Ÿ”‘', '๐Ÿ—๏ธ', '๐Ÿ›ก๏ธ', '๐Ÿšจ', 'โš ๏ธ', '๐Ÿ”ฑ'],
'media': ['๐Ÿ“ท', '๐Ÿ“ธ', '๐Ÿ“น', '๐ŸŽฅ', '๐Ÿ“ฝ๏ธ', '๐ŸŽž๏ธ', '๐Ÿ“ป', '๐Ÿ“บ', '๐Ÿ“ผ', '๐Ÿ’ฟ', '๐Ÿ“€', '๐Ÿ’พ', '๐Ÿ’ฝ'],
'gaming': ['๐ŸŽฎ', '๐Ÿ•น๏ธ', '๐ŸŽฏ', '๐ŸŽฒ', '๐Ÿƒ', '๐ŸŽฐ', '๐Ÿงฉ', 'โ™ ๏ธ', 'โ™ฅ๏ธ', 'โ™ฆ๏ธ', 'โ™ฃ๏ธ'],
'social': ['๐Ÿ‘ฅ', '๐Ÿ‘ค', '๐Ÿ’ฌ', '๐Ÿ—ฃ๏ธ', '๐Ÿ‘‹', '๐Ÿค', '๐Ÿ’ช', '๐Ÿ‘', '๐Ÿ‘', '๐Ÿ™Œ', '๐Ÿคณ', '๐Ÿ“ฑ'],
'news': ['๐Ÿ“ฐ', '๐Ÿ“ก', '๐Ÿ“ป', '๐Ÿ“บ', '๐Ÿ—ž๏ธ', '๐Ÿ“ข', '๐Ÿ“ฃ', '๐Ÿ””', '๐Ÿ”•'],
'design': ['๐ŸŽจ', '๐Ÿ–Œ๏ธ', '๐Ÿ–๏ธ', 'โœ๏ธ', '๐Ÿ“', '๐Ÿ“', '๐Ÿ–ผ๏ธ', '๐ŸŽญ', '๐ŸŒˆ', 'โœจ'],
'cloud': ['โ˜๏ธ', 'โ›…', '๐ŸŒค๏ธ', '๐ŸŒฅ๏ธ', '๐ŸŒฆ๏ธ', '๐ŸŒง๏ธ', '๐Ÿ’พ', '๐Ÿ’ฟ', '๐Ÿ“€', '๐Ÿ—„๏ธ'],
'general': ['๐Ÿ“ฆ', '๐Ÿ“‹', '๐Ÿ“Œ', '๐Ÿ“', '๐Ÿ”–', '๐Ÿท๏ธ', '๐Ÿ’ก', '๐Ÿ”†', 'โญ', 'โœจ', '๐ŸŽฏ', '๐ŸŽช', '๐ŸŽก', '๐ŸŽข']
};
const allEmojis = [];
Object.keys(emojiDatabase).forEach(category => {
emojiDatabase[category].forEach(emoji => {
if (!allEmojis.find(e => e.emoji === emoji)) {
allEmojis.push({ emoji, category });
}
});
});
function displayEmojis(emojis) {
const emojiGrid = document.getElementById('emojiGrid');
emojiGrid.innerHTML = '';
if (emojis.length === 0) {
emojiGrid.innerHTML = '<p style="text-align:center; color:#666; grid-column: 1/-1;">No emojis found</p>';
return;
}
emojis.forEach(({ emoji }) => {
const option = document.createElement('div');
option.className = 'emoji-option';
option.textContent = emoji;
option.onclick = () => {
iconInput.value = emoji;
modal.style.display = 'none';
};
emojiGrid.appendChild(option);
});
}
function findRelevantEmojis(searchTerm) {
const term = searchTerm.toLowerCase().trim();
if (!term) return allEmojis.slice(0, 50);
const categoryMatch = Object.keys(emojiDatabase).find(cat =>
cat.includes(term) || term.includes(cat)
);
if (categoryMatch) {
return emojiDatabase[categoryMatch].map(emoji => ({ emoji, category: categoryMatch }));
}
const keywords = {
'netflix': ['streaming', 'entertainment'],
'spotify': ['music'],
'apple': ['technology'],
'amazon': ['shopping'],
'prime': ['streaming', 'shopping'],
'youtube': ['streaming', 'entertainment'],
'gym': ['fitness'],
'hulu': ['streaming', 'entertainment'],
'disney': ['streaming', 'entertainment'],
'google': ['technology', 'cloud'],
'microsoft': ['technology', 'cloud'],
'adobe': ['design'],
'figma': ['design'],
'slack': ['communication', 'business'],
'notion': ['business', 'education'],
'dropbox': ['cloud'],
'vpn': ['security'],
'news': ['news'],
'podcast': ['media', 'music']
};
for (let [keyword, categories] of Object.entries(keywords)) {
if (term.includes(keyword) || keyword.includes(term)) {
let results = [];
categories.forEach(cat => {
if (emojiDatabase[cat]) {
results.push(...emojiDatabase[cat].map(emoji => ({ emoji, category: cat })));
}
});
return results;
}
}
return emojiDatabase['general'].map(emoji => ({ emoji, category: 'general' }));
}
emojiSearchBtn.addEventListener('click', () => {
const searchTerm = nameInput.value.split(' ')[0] || 'general';
const emojiDbLink = document.getElementById('emojiDbLink');
emojiDbLink.href = `https://emojidb.org/${searchTerm.toLowerCase()}-emojis`;
const relevantEmojis = findRelevantEmojis(searchTerm);
displayEmojis(relevantEmojis);
modal.style.display = 'flex';
});
document.getElementById('emojiSearchInput').addEventListener('input', (e) => {
const searchValue = e.target.value;
const relevantEmojis = findRelevantEmojis(searchValue);
displayEmojis(relevantEmojis);
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.style.display = 'none';
}
});
updateVisualization();
window.addEventListener('resize', () => {
if (subscriptions.length > 0) {
updateVisualization();
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment