Skip to content
This repository was archived by the owner on Jul 28, 2026. It is now read-only.

Commit 69e270e

Browse files
committed
feat: load all 3300+ Simple Icons dynamically from GitHub
Replace hardcoded icon list with async fetch from Simple Icons repo. Icons are parsed from slugs.md and cached after first load. Picker shows loading spinner while icons load.
1 parent a01ff74 commit 69e270e

1 file changed

Lines changed: 81 additions & 168 deletions

File tree

public/js/admin.js

Lines changed: 81 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -20,153 +20,49 @@ function ahtml(strings, ...values) {
2020
return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
2121
}
2222

23-
// ─── Simple Icons Library ─────────────────────────────────
24-
const SIMPLE_ICONS = [
25-
{ slug: 'docker', label: 'Docker' },
26-
{ slug: 'kubernetes', label: 'Kubernetes' },
27-
{ slug: 'amazoneks', label: 'EKS' },
28-
{ slug: 'terraform', label: 'Terraform' },
29-
{ slug: 'ansible', label: 'Ansible' },
30-
{ slug: 'database', label: 'Database' },
31-
{ slug: 'code', label: 'Code' },
32-
{ slug: 'nginx', label: 'Nginx' },
33-
{ slug: 'apache', label: 'Apache' },
34-
{ slug: 'caddy', label: 'Caddy' },
35-
{ slug: 'traefik', label: 'Traefik' },
36-
{ slug: 'ubuntu', label: 'Ubuntu' },
37-
{ slug: 'debian', label: 'Debian' },
38-
{ slug: 'centos', label: 'CentOS' },
39-
{ slug: 'fedora', label: 'Fedora' },
40-
{ slug: 'alpinelinux', label: 'Alpine' },
41-
{ slug: 'archlinux', label: 'Arch' },
42-
{ slug: 'opensuse', label: 'openSUSE' },
43-
{ slug: 'freebsd', label: 'FreeBSD' },
44-
{ slug: 'linux', label: 'Linux' },
45-
{ slug: 'windows', label: 'Windows' },
46-
{ slug: 'python', label: 'Python' },
47-
{ slug: 'javascript', label: 'JavaScript' },
48-
{ slug: 'typescript', label: 'TypeScript' },
49-
{ slug: 'java', label: 'Java' },
50-
{ slug: 'go', label: 'Go' },
51-
{ slug: 'rust', label: 'Rust' },
52-
{ slug: 'php', label: 'PHP' },
53-
{ slug: 'ruby', label: 'Ruby' },
54-
{ slug: 'cplusplus', label: 'C++' },
55-
{ slug: 'csharp', label: 'C#' },
56-
{ slug: 'kotlin', label: 'Kotlin' },
57-
{ slug: 'swift', label: 'Swift' },
58-
{ slug: 'lua', label: 'Lua' },
59-
{ slug: 'perl', label: 'Perl' },
60-
{ slug: 'scala', label: 'Scala' },
61-
{ slug: 'haskell', label: 'Haskell' },
62-
{ slug: 'elixir', label: 'Elixir' },
63-
{ slug: 'nodejs', label: 'Node.js' },
64-
{ slug: 'nodedotjs', label: 'Node.js' },
65-
{ slug: 'react', label: 'React' },
66-
{ slug: 'vue-dot-js', label: 'Vue.js' },
67-
{ slug: 'angular', label: 'Angular' },
68-
{ slug: 'nextdotjs', label: 'Next.js' },
69-
{ slug: 'svelte', label: 'Svelte' },
70-
{ slug: 'express', label: 'Express' },
71-
{ slug: 'flask', label: 'Flask' },
72-
{ slug: 'django', label: 'Django' },
73-
{ slug: 'laravel', label: 'Laravel' },
74-
{ slug: 'spring', label: 'Spring' },
75-
{ slug: 'rails', label: 'Rails' },
76-
{ slug: 'fastapi', label: 'FastAPI' },
77-
{ slug: 'mysql', label: 'MySQL' },
78-
{ slug: 'postgresql', label: 'PostgreSQL' },
79-
{ slug: 'mongodb', label: 'MongoDB' },
80-
{ slug: 'redis', label: 'Redis' },
81-
{ slug: 'sqlite', label: 'SQLite' },
82-
{ slug: 'mariadb', label: 'MariaDB' },
83-
{ slug: 'elasticsearch', label: 'Elastic' },
84-
{ slug: 'cassandra', label: 'Cassandra' },
85-
{ slug: 'neo4j', label: 'Neo4j' },
86-
{ slug: 'influxdb', label: 'InfluxDB' },
87-
{ slug: 'firebase', label: 'Firebase' },
88-
{ slug: 'supabase', label: 'Supabase' },
89-
{ slug: 'prisma', label: 'Prisma' },
90-
{ slug: 'minecraft', label: 'Minecraft' },
91-
{ slug: 'terraria', label: 'Terraria' },
92-
{ slug: 'valheim', label: 'Valheim' },
93-
{ slug: 'factorio', label: 'Factorio' },
94-
{ slug: 'amongus', label: 'Among Us' },
95-
{ slug: 'steam', label: 'Steam' },
96-
{ slug: 'garrysmod', label: 'Garry\'s Mod' },
97-
{ slug: 'teamfortress', label: 'Team Fortress' },
98-
{ slug: 'left4dead', label: 'L4D' },
99-
{ slug: 'halflife', label: 'Half-Life' },
100-
{ slug: 'palworld', label: 'Palworld' },
101-
{ slug: 'stardewvalley', label: 'Stardew' },
102-
{ slug: 'dontstarve', label: 'Don\'t Starve' },
103-
{ slug: 'callofduty', label: 'Call of Duty' },
104-
{ slug: 'arma', label: 'Arma' },
105-
{ slug: 'dayz', label: 'DayZ' },
106-
{ slug: 'rustgame', label: 'Rust (Game)' },
107-
{ slug: 'subnautica', label: 'Subnautica' },
108-
{ slug: 'ark', label: 'ARK' },
109-
{ slug: 'minecraftbedrock', label: 'MC Bedrock' },
110-
{ slug: 'nodebb', label: 'NodeBB' },
111-
{ slug: 'flarum', label: 'Flarum' },
112-
{ slug: 'ghost', label: 'Ghost' },
113-
{ slug: 'wordpress', label: 'WordPress' },
114-
{ slug: 'drupal', label: 'Drupal' },
115-
{ slug: 'joomla', label: 'Joomla' },
116-
{ slug: 'grafana', label: 'Grafana' },
117-
{ slug: 'prometheus', label: 'Prometheus' },
118-
{ slug: 'zabbix', label: 'Zabbix' },
119-
{ slug: 'portainer', label: 'Portainer' },
120-
{ slug: 'jenkins', label: 'Jenkins' },
121-
{ slug: 'gitlab', label: 'GitLab' },
122-
{ slug: 'github', label: 'GitHub' },
123-
{ slug: 'gitea', label: 'Gitea' },
124-
{ slug: 'git', label: 'Git' },
125-
{ slug: 'visualstudiocode', label: 'VS Code' },
126-
{ slug: 'postman', label: 'Postman' },
127-
{ slug: 'cloudflare', label: 'Cloudflare' },
128-
{ slug: 'vercel', label: 'Vercel' },
129-
{ slug: 'netlify', label: 'Netlify' },
130-
{ slug: 'heroku', label: 'Heroku' },
131-
{ slug: 'digitalocean', label: 'DigitalOcean' },
132-
{ slug: 'vultr', label: 'Vultr' },
133-
{ slug: 'hetzner', label: 'Hetzner' },
134-
{ slug: 'oracle', label: 'Oracle' },
135-
{ slug: 'linode', label: 'Linode' },
136-
{ slug: 'amazonwebservices', label: 'AWS' },
137-
{ slug: 'microsoftazure', label: 'Azure' },
138-
{ slug: 'googlecloud', label: 'GCP' },
139-
{ slug: 'openai', label: 'OpenAI' },
140-
{ slug: 'discord', label: 'Discord' },
141-
{ slug: 'telegram', label: 'Telegram' },
142-
{ slug: 'slack', label: 'Slack' },
143-
{ slug: 'plex', label: 'Plex' },
144-
{ slug: 'jellyfin', label: 'Jellyfin' },
145-
{ slug: 'emby', label: 'Emby' },
146-
{ slug: 'nextcloud', label: 'Nextcloud' },
147-
{ slug: 'vaultwarden', label: 'Vaultwarden' },
148-
{ slug: 'wireguard', label: 'WireGuard' },
149-
{ slug: 'openvpn', label: 'OpenVPN' },
150-
{ slug: 'pihole', label: 'Pi-hole' },
151-
{ slug: 'ubiquiti', label: 'Ubiquiti' },
152-
{ slug: 'homeassistant', label: 'Home Assistant' },
153-
{ slug: 'tautulli', label: 'Tautulli' },
154-
{ slug: 'sonarqube', label: 'SonarQube' },
155-
{ slug: 'verdaccio', label: 'Verdaccio' },
156-
{ slug: 'minio', label: 'MinIO' },
157-
{ slug: 'rabbitmq', label: 'RabbitMQ' },
158-
{ slug: 'kafka', label: 'Kafka' },
159-
{ slug: 'nats', label: 'NATS' },
160-
{ slug: 'haproxy', label: 'HAProxy' },
161-
{ slug: 'consul', label: 'Consul' },
162-
{ slug: 'nomad', label: 'Nomad' },
163-
{ slug: 'etcd', label: 'etcd' },
164-
{ slug: 'podman', label: 'Podman' },
165-
{ slug: 'lxc', label: 'LXC' },
166-
{ slug: 'proxmox', label: 'Proxmox' },
167-
{ slug: 'vmware', label: 'VMware' },
168-
{ slug: 'vagrant', label: 'Vagrant' },
169-
];
23+
// ─── Simple Icons Library (loaded dynamically) ────────────
24+
let _simpleIconsCache = null;
25+
let _simpleIconsLoading = null;
26+
27+
async function loadSimpleIcons() {
28+
if (_simpleIconsCache) return _simpleIconsCache;
29+
if (_simpleIconsLoading) return _simpleIconsLoading;
30+
31+
_simpleIconsLoading = (async () => {
32+
try {
33+
const res = await fetch('https://raw.githubusercontent.com/simple-icons/simple-icons/develop/slugs.md');
34+
const text = await res.text();
35+
const icons = [];
36+
const re = /\|\s*`([^`]+)`\s*\|\s*`([^`]+)`\s*\|/g;
37+
let m;
38+
while ((m = re.exec(text)) !== null) {
39+
const label = m[1];
40+
const slug = m[2];
41+
if (slug && label) icons.push({ slug, label });
42+
}
43+
icons.sort((a, b) => a.label.localeCompare(b.label));
44+
_simpleIconsCache = icons;
45+
} catch (e) {
46+
console.warn('Failed to load Simple Icons list, using fallback');
47+
_simpleIconsCache = [
48+
{ slug: 'docker', label: 'Docker' },
49+
{ slug: 'kubernetes', label: 'Kubernetes' },
50+
{ slug: 'minecraft', label: 'Minecraft' },
51+
{ slug: 'ubuntu', label: 'Ubuntu' },
52+
{ slug: 'debian', label: 'Debian' },
53+
{ slug: 'linux', label: 'Linux' },
54+
{ slug: 'github', label: 'GitHub' },
55+
{ slug: 'discord', label: 'Discord' },
56+
{ slug: 'database', label: 'Database' },
57+
{ slug: 'code', label: 'Code' },
58+
];
59+
}
60+
_simpleIconsLoading = null;
61+
return _simpleIconsCache;
62+
})();
63+
64+
return _simpleIconsLoading;
65+
}
17066

17167
function siUrl(slug) {
17268
return `https://cdn.simpleicons.org/${slug}`;
@@ -184,7 +80,12 @@ function renderLogoDisplay(logo, size, extraStyle) {
18480
}
18581

18682
function renderSvgPickerGrid(selectedSlug) {
187-
return SIMPLE_ICONS.map(icon => {
83+
return `<div style="text-align:center;padding:16px;color:var(--text-secondary)"><span class="spinner"></span> Loading icons...</div>`;
84+
}
85+
86+
async function renderSvgPickerGridLoaded(selectedSlug) {
87+
const icons = await loadSimpleIcons();
88+
return icons.map(icon => {
18889
const isSelected = icon.slug === selectedSlug;
18990
return `<div class="svg-picker-item ${isSelected ? 'selected' : ''}" data-slug="${icon.slug}" data-label="${escapeHtml(icon.label)}" title="${escapeHtml(icon.label)}">
19091
<img src="${siUrl(icon.slug)}" alt="${escapeHtml(icon.label)}" loading="lazy" />
@@ -1965,16 +1866,6 @@ async function renderAdminEggSettings(nestId, eggId) {
19651866
});
19661867
});
19671868

1968-
initSvgPickerListeners($a('#egg-svg-picker'), (slug) => {
1969-
$a('#egg-svg-slug').value = slug;
1970-
const prev = $a('#egg-svg-selected-preview');
1971-
if (prev) {
1972-
prev.style.display = 'flex';
1973-
prev.querySelector('img').src = siUrl(slug);
1974-
prev.querySelector('.logo-preview-label').textContent = slug;
1975-
}
1976-
});
1977-
19781869
$a('#egg-svg-clear')?.addEventListener('click', () => {
19791870
$a('#egg-svg-slug').value = '';
19801871
const prev = $a('#egg-svg-selected-preview');
@@ -2054,6 +1945,22 @@ async function renderAdminEggSettings(nestId, eggId) {
20541945

20551946
initIcons();
20561947

1948+
(async () => {
1949+
const grid = $a('#egg-svg-picker .svg-picker-grid');
1950+
if (grid) {
1951+
grid.innerHTML = await renderSvgPickerGridLoaded('');
1952+
initSvgPickerListeners($a('#egg-svg-picker'), (slug) => {
1953+
$a('#egg-svg-slug').value = slug;
1954+
const prev = $a('#egg-svg-selected-preview');
1955+
if (prev) {
1956+
prev.style.display = 'flex';
1957+
prev.querySelector('img').src = siUrl(slug);
1958+
prev.querySelector('.logo-preview-label').textContent = slug;
1959+
}
1960+
});
1961+
}
1962+
})();
1963+
20571964
$a('#btn-save-egg-resources-all')?.addEventListener('click', async () => {
20581965
if (!confirm('Apply these resources to ALL existing servers using this egg? This will update their limits on the panel.')) return;
20591966
const btn = $a('#btn-save-egg-resources-all');
@@ -2247,6 +2154,22 @@ function showRenameNestModal(nestId, currentName, currentLogo, currentDescriptio
22472154
overlay.style.display = 'flex';
22482155
initIcons();
22492156

2157+
(async () => {
2158+
const grid = $a('#nest-svg-picker .svg-picker-grid');
2159+
if (grid) {
2160+
grid.innerHTML = await renderSvgPickerGridLoaded(initialSvgSlug);
2161+
initSvgPickerListeners($a('#nest-svg-picker'), (slug) => {
2162+
$a('#modal-edit-nest-svg-slug').value = slug;
2163+
const prev = $a('#nest-svg-selected-preview');
2164+
if (prev) {
2165+
prev.style.display = 'flex';
2166+
prev.querySelector('img').src = siUrl(slug);
2167+
prev.querySelector('.logo-preview-label').textContent = slug;
2168+
}
2169+
});
2170+
}
2171+
})();
2172+
22502173
const toggleBtns = content.querySelectorAll('.logo-type-toggle button');
22512174
toggleBtns.forEach(btn => {
22522175
btn.addEventListener('click', () => {
@@ -2271,16 +2194,6 @@ function showRenameNestModal(nestId, currentName, currentLogo, currentDescriptio
22712194
}
22722195
});
22732196

2274-
initSvgPickerListeners($a('#nest-svg-picker'), (slug) => {
2275-
$a('#modal-edit-nest-svg-slug').value = slug;
2276-
const prev = $a('#nest-svg-selected-preview');
2277-
if (prev) {
2278-
prev.style.display = 'flex';
2279-
prev.querySelector('img').src = siUrl(slug);
2280-
prev.querySelector('.logo-preview-label').textContent = slug;
2281-
}
2282-
});
2283-
22842197
$a('#nest-svg-clear')?.addEventListener('click', () => {
22852198
$a('#modal-edit-nest-svg-slug').value = '';
22862199
const prev = $a('#nest-svg-selected-preview');

0 commit comments

Comments
 (0)