feat: community gallery UI (auth modals, stack cards, star/use, share), search, presets, dependency/port warnings

This commit is contained in:
2026-08-03 23:42:19 +01:00
parent 9a98985d3e
commit 46906c46b3
5 changed files with 451 additions and 1 deletions

146
src/utils/gallery.js Normal file
View File

@@ -0,0 +1,146 @@
import { getToken, isLoggedIn } from './auth.js';
const API_URL = '/api';
let galleryStacks = [];
export function initGallery(getSelectedApps, getConfig) {
loadGallery();
document.getElementById('gallery-search').addEventListener('input', debounce(loadGallery, 300));
document.getElementById('gallery-sort').addEventListener('change', loadGallery);
// Share button
document.getElementById('share-stack-btn').addEventListener('click', () => {
if (!isLoggedIn()) {
document.getElementById('auth-modal').classList.remove('hidden');
return;
}
if (getSelectedApps().length === 0) {
alert('Select some apps in the Stack Builder first!');
return;
}
document.getElementById('share-modal').classList.remove('hidden');
});
document.getElementById('share-modal-close').addEventListener('click', () => {
document.getElementById('share-modal').classList.add('hidden');
});
document.getElementById('share-form').addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('share-name').value.trim();
const desc = document.getElementById('share-desc').value.trim();
const tags = document.getElementById('share-tags').value.split(',').map(t => t.trim()).filter(Boolean);
const res = await fetch(`${API_URL}/stacks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({
name,
description: desc,
apps: getSelectedApps(),
config: getConfig(),
tags,
}),
});
if (res.ok) {
document.getElementById('share-modal').classList.add('hidden');
document.getElementById('share-form').reset();
loadGallery();
}
});
}
async function loadGallery() {
const search = document.getElementById('gallery-search').value.trim();
const sort = document.getElementById('gallery-sort').value;
let url = `${API_URL}/stacks?sort=${sort}&limit=30`;
if (search) url += `&search=${encodeURIComponent(search)}`;
try {
const res = await fetch(url, {
headers: getToken() ? { Authorization: `Bearer ${getToken()}` } : {},
});
if (res.ok) {
galleryStacks = await res.json();
renderGallery();
}
} catch (e) {
document.getElementById('gallery-grid').innerHTML = '<p style="color:var(--text-muted)">Unable to load community stacks. Is the API running?</p>';
}
}
function renderGallery() {
const grid = document.getElementById('gallery-grid');
if (galleryStacks.length === 0) {
grid.innerHTML = '<p style="color:var(--text-muted)">No stacks shared yet. Be the first!</p>';
return;
}
grid.innerHTML = galleryStacks.map(stack => `
<div class="stack-card" data-id="${stack.id}">
<div class="stack-name">${esc(stack.name)}</div>
<div class="stack-desc">${esc(stack.description || '')}</div>
<div class="stack-apps">${stack.apps.slice(0, 8).map(a => `<span class="stack-app-tag">${a}</span>`).join('')}${stack.apps.length > 8 ? `<span class="stack-app-tag">+${stack.apps.length - 8}</span>` : ''}</div>
<div class="stack-meta">
<span>⭐ ${stack.stars}</span>
<span>📦 ${stack.downloads}</span>
<span>by ${esc(stack.display_name || stack.username)}</span>
</div>
<div class="stack-actions">
<button class="stack-action-btn${stack.starred_by_me ? ' starred' : ''}" data-action="star" data-id="${stack.id}">${stack.starred_by_me ? '⭐ Starred' : '☆ Star'}</button>
<button class="stack-action-btn" data-action="use" data-id="${stack.id}">🚀 Use This Stack</button>
</div>
</div>
`).join('');
// Event listeners
grid.querySelectorAll('[data-action="star"]').forEach(btn => {
btn.addEventListener('click', () => starStack(btn.dataset.id));
});
grid.querySelectorAll('[data-action="use"]').forEach(btn => {
btn.addEventListener('click', () => useStack(btn.dataset.id));
});
}
async function starStack(id) {
if (!isLoggedIn()) {
document.getElementById('auth-modal').classList.remove('hidden');
return;
}
await fetch(`${API_URL}/stacks/${id}/star`, {
method: 'POST',
headers: { Authorization: `Bearer ${getToken()}` },
});
loadGallery();
}
async function useStack(id) {
const stack = galleryStacks.find(s => s.id === parseInt(id));
if (!stack) return;
// Track download
fetch(`${API_URL}/stacks/${id}/download`, { method: 'POST' });
// Dispatch event for main.js to load these apps
window.dispatchEvent(new CustomEvent('use-stack', { detail: { apps: stack.apps, config: stack.config } }));
// Switch to builder mode
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
document.querySelector('.mode-btn[data-mode="builder"]').classList.add('active');
document.querySelector('.container').style.display = 'block';
document.getElementById('step-gallery').style.display = 'none';
document.getElementById('step-analyser').style.display = 'none';
}
function esc(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function debounce(fn, ms) {
let timer;
return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); };
}