147 lines
5.2 KiB
JavaScript
147 lines
5.2 KiB
JavaScript
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); };
|
|
}
|