feat: community gallery UI (auth modals, stack cards, star/use, share), search, presets, dependency/port warnings
This commit is contained in:
146
src/utils/gallery.js
Normal file
146
src/utils/gallery.js
Normal 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function debounce(fn, ms) {
|
||||
let timer;
|
||||
return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); };
|
||||
}
|
||||
Reference in New Issue
Block a user