diff --git a/welcome/app.js b/welcome/app.js index 1e2c98a..ee31acf 100644 --- a/welcome/app.js +++ b/welcome/app.js @@ -1,13 +1,148 @@ /** * n8n-install Welcome Page * Dynamic rendering of services and credentials from data.json - * Supabase-inspired design + * Supabase-inspired design with cinematic animations */ (function() { 'use strict'; - // Service metadata - hardcoded info about each service + // ============================================ + // CINEMATIC ANIMATIONS MODULE + // ============================================ + const CinematicAnimations = { + CONFETTI_STORAGE_KEY: 'n8n_install_welcomed', + + isFirstVisit() { + return !localStorage.getItem(this.CONFETTI_STORAGE_KEY); + }, + + markVisited() { + localStorage.setItem(this.CONFETTI_STORAGE_KEY, Date.now().toString()); + }, + + triggerConfetti() { + if (!window.confetti) return; + + const colors = ['#3ECF8E', '#24B374', '#47DF97', '#ffffff', '#75E7B1']; + const defaults = { + spread: 60, + ticks: 100, + gravity: 1, + decay: 0.94, + startVelocity: 30, + colors: colors + }; + + function fire(particleRatio, opts) { + confetti({ + ...defaults, + ...opts, + particleCount: Math.floor(200 * particleRatio) + }); + } + + // Staggered confetti bursts + fire(0.25, { spread: 26, startVelocity: 55 }); + fire(0.2, { spread: 60 }); + fire(0.35, { spread: 100, decay: 0.91, scalar: 0.8 }); + fire(0.1, { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 }); + fire(0.1, { spread: 120, startVelocity: 45 }); + + // Side cannons + setTimeout(() => { + confetti({ + particleCount: 50, + angle: 60, + spread: 55, + origin: { x: 0 }, + colors: colors + }); + confetti({ + particleCount: 50, + angle: 120, + spread: 55, + origin: { x: 1 }, + colors: colors + }); + }, 250); + }, + + init() { + // Check for first visit and trigger confetti + if (this.isFirstVisit()) { + setTimeout(() => { + this.triggerConfetti(); + this.markVisited(); + }, 800); + } + } + }; + + // ============================================ + // ICONS - SVG icons as template functions + // ============================================ + const Icons = { + copy: (className = '') => ` + `, + + check: (className = '') => ` + `, + + eyeOpen: (className = '') => ` + `, + + eyeClosed: (className = '') => ` + `, + + externalLink: (className = '') => ` + `, + + server: (className = '') => ` + `, + + bolt: (className = '') => ` + `, + + refresh: (className = '') => ` + `, + + terminal: (className = '') => ` + `, + + book: (className = '') => ` + `, + + warning: (className = '') => ` + ` + }; + + // ============================================ + // DATA - Service metadata and commands + // ============================================ const SERVICE_METADATA = { 'n8n': { name: 'n8n', @@ -228,7 +363,6 @@ } }; - // Make commands data const COMMANDS = [ { cmd: 'make status', desc: 'Show container status' }, { cmd: 'make logs', desc: 'View logs (all services)' }, @@ -241,371 +375,9 @@ { cmd: 'make clean', desc: 'Remove unused Docker resources' } ]; - // DOM Elements - const servicesContainer = document.getElementById('services-container'); - const quickstartContainer = document.getElementById('quickstart-container'); - const commandsContainer = document.getElementById('commands-container'); - const domainInfo = document.getElementById('domain-info'); - const errorToast = document.getElementById('error-toast'); - const errorMessage = document.getElementById('error-message'); - - /** - * Create a copy button for any text - */ - function createCopyButton(textToCopy) { - const copyBtn = document.createElement('button'); - copyBtn.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50'; - copyBtn.innerHTML = ` - - - - - `; - copyBtn.title = 'Copy to clipboard'; - - copyBtn.addEventListener('click', async () => { - try { - await navigator.clipboard.writeText(textToCopy); - const copyIcon = copyBtn.querySelector('.copy-icon'); - const checkIcon = copyBtn.querySelector('.check-icon'); - copyIcon.classList.add('hidden'); - checkIcon.classList.remove('hidden'); - setTimeout(() => { - copyIcon.classList.remove('hidden'); - checkIcon.classList.add('hidden'); - }, 2000); - } catch (err) { - console.error('Failed to copy:', err); - } - }); - - return copyBtn; - } - - /** - * Show error toast - */ - function showError(message) { - errorMessage.textContent = message; - errorToast.classList.remove('hidden'); - setTimeout(() => { - errorToast.classList.remove('translate-y-20', 'opacity-0'); - }, 10); - - setTimeout(() => { - errorToast.classList.add('translate-y-20', 'opacity-0'); - setTimeout(() => errorToast.classList.add('hidden'), 300); - }, 5000); - } - - /** - * Create password field with toggle and copy buttons - */ - function createPasswordField(password) { - const container = document.createElement('div'); - container.className = 'flex items-center gap-1'; - - const passwordSpan = document.createElement('span'); - passwordSpan.className = 'font-mono text-sm select-all text-gray-300'; - passwordSpan.textContent = '*'.repeat(Math.min(password.length, 12)); - passwordSpan.dataset.password = password; - passwordSpan.dataset.hidden = 'true'; - - // Toggle visibility button (eye icon) - const toggleBtn = document.createElement('button'); - toggleBtn.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50'; - toggleBtn.innerHTML = ` - - - - - - `; - toggleBtn.title = 'Toggle visibility'; - - // Toggle password visibility on click - toggleBtn.addEventListener('click', () => { - const isHidden = passwordSpan.dataset.hidden === 'true'; - const eyeOpen = toggleBtn.querySelector('.eye-open'); - const eyeClosed = toggleBtn.querySelector('.eye-closed'); - - if (isHidden) { - passwordSpan.textContent = passwordSpan.dataset.password; - passwordSpan.dataset.hidden = 'false'; - eyeOpen.classList.add('hidden'); - eyeClosed.classList.remove('hidden'); - } else { - passwordSpan.textContent = '*'.repeat(Math.min(password.length, 12)); - passwordSpan.dataset.hidden = 'true'; - eyeOpen.classList.remove('hidden'); - eyeClosed.classList.add('hidden'); - } - }); - - // Copy button - const copyBtn = document.createElement('button'); - copyBtn.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50'; - copyBtn.innerHTML = ` - - - - - `; - copyBtn.title = 'Copy to clipboard'; - - copyBtn.addEventListener('click', async () => { - try { - await navigator.clipboard.writeText(password); - // Show checkmark - const copyIcon = copyBtn.querySelector('.copy-icon'); - const checkIcon = copyBtn.querySelector('.check-icon'); - copyIcon.classList.add('hidden'); - checkIcon.classList.remove('hidden'); - // Revert after 2 seconds - setTimeout(() => { - copyIcon.classList.remove('hidden'); - checkIcon.classList.add('hidden'); - }, 2000); - } catch (err) { - console.error('Failed to copy:', err); - } - }); - - container.appendChild(passwordSpan); - container.appendChild(toggleBtn); - container.appendChild(copyBtn); - - return container; - } - - /** - * Render a single service card - */ - function renderServiceCard(key, serviceData) { - const metadata = SERVICE_METADATA[key] || { - name: key, - description: '', - icon: key.substring(0, 2).toUpperCase(), - color: 'bg-gray-600' - }; - - const card = document.createElement('div'); - card.className = 'bg-surface-100 rounded-xl border border-surface-400 p-5 hover:border-brand/30 hover:bg-surface-200 transition-all'; - - // Build credentials section - let credentialsHtml = ''; - if (serviceData.credentials) { - const creds = serviceData.credentials; - - if (creds.note) { - credentialsHtml = ` -
-

${escapeHtml(creds.note)}

-
- `; - } else { - let fields = []; - if (creds.username) { - fields.push(` -
- Username: -
- ${escapeHtml(creds.username)} -
-
- `); - } - if (creds.password) { - fields.push(` -
- Password: -
- `); - } - if (creds.api_key) { - fields.push(` -
- API Key: -
- `); - } - - if (fields.length > 0) { - credentialsHtml = ` -
- ${fields.join('')} -
- `; - } - } - } - - // Build extra info section (internal URLs, etc.) - let extraHtml = ''; - if (serviceData.extra) { - const extraItems = []; - const extra = serviceData.extra; - - if (extra.internal_api) { - extraItems.push(`Internal: ${escapeHtml(extra.internal_api)}`); - } - if (extra.internal_url) { - extraItems.push(`Internal: ${escapeHtml(extra.internal_url)}`); - } - if (extra.workers) { - extraItems.push(`Workers: ${escapeHtml(extra.workers)}`); - } - if (extra.recommendation) { - extraItems.push(`${escapeHtml(extra.recommendation)}`); - } - - if (extraItems.length > 0) { - extraHtml = `
${extraItems.join('')}
`; - } - } - - card.innerHTML = ` -
-
- ${metadata.icon} -
-
-

${escapeHtml(metadata.name)}

-

${escapeHtml(metadata.description)}

- ${serviceData.hostname ? ` - - ${escapeHtml(serviceData.hostname)} - - - - - ` : 'Internal service'} - ${extraHtml} -
-
- ${credentialsHtml} - `; - - // Add password fields and copy buttons after card is created - if (serviceData.credentials) { - const creds = serviceData.credentials; - - setTimeout(() => { - if (creds.username) { - const userContainer = card.querySelector(`#user-${key} .flex.items-center`); - if (userContainer) { - userContainer.appendChild(createCopyButton(creds.username)); - } - } - if (creds.password) { - const pwdContainer = card.querySelector(`#pwd-${key}`); - if (pwdContainer) { - pwdContainer.appendChild(createPasswordField(creds.password)); - } - } - if (creds.api_key) { - const apiContainer = card.querySelector(`#api-${key}`); - if (apiContainer) { - apiContainer.appendChild(createPasswordField(creds.api_key)); - } - } - }, 0); - } - - return card; - } - - /** - * Render all services - */ - function renderServices(services) { - servicesContainer.innerHTML = ''; - - if (!services || Object.keys(services).length === 0) { - servicesContainer.innerHTML = ` -
-

No services configured. Run the installer to set up services.

-
- `; - return; - } - - // Sort services: external first (with hostname), then internal - const sortedKeys = Object.keys(services).sort((a, b) => { - const aHasHostname = services[a].hostname ? 1 : 0; - const bHasHostname = services[b].hostname ? 1 : 0; - return bHasHostname - aHasHostname; - }); - - sortedKeys.forEach(key => { - servicesContainer.appendChild(renderServiceCard(key, services[key])); - }); - } - - /** - * Render quick start steps - */ - function renderQuickStart(steps) { - quickstartContainer.innerHTML = ''; - - if (!steps || steps.length === 0) { - // Default steps if none provided - steps = [ - { step: 1, title: 'Log into n8n', description: 'Use the email you provided during installation' }, - { step: 2, title: 'Create your first workflow', description: 'Start with a Manual Trigger + HTTP Request nodes' }, - { step: 3, title: 'Explore community workflows', description: 'Check imported workflows for 300+ examples' }, - { step: 4, title: 'Monitor your system', description: 'Use Grafana to track performance' } - ]; - } - - steps.forEach(item => { - const stepEl = document.createElement('div'); - stepEl.className = 'flex items-start gap-4 p-4 bg-surface-100 rounded-xl border border-surface-400 hover:border-brand/30 transition-all'; - - stepEl.innerHTML = ` -
- ${item.step} -
-
-

${escapeHtml(item.title)}

-

${escapeHtml(item.description)}

-
- `; - - quickstartContainer.appendChild(stepEl); - }); - } - - /** - * Render make commands - */ - function renderCommands() { - commandsContainer.innerHTML = ''; - - const grid = document.createElement('div'); - grid.className = 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'; - - COMMANDS.forEach(item => { - const cmdEl = document.createElement('div'); - cmdEl.className = 'flex flex-col gap-1 p-3 rounded-lg bg-surface-200/50 border border-surface-400 hover:border-brand/30 transition-all'; - - cmdEl.innerHTML = ` - ${escapeHtml(item.cmd)} - ${escapeHtml(item.desc)} - `; - - grid.appendChild(cmdEl); - }); - - commandsContainer.appendChild(grid); - } + // ============================================ + // UTILS - Helper functions + // ============================================ /** * Escape HTML to prevent XSS @@ -617,10 +389,459 @@ return div.innerHTML; } + /** + * Copy text to clipboard and show visual feedback + */ + async function copyToClipboard(text, button) { + try { + await navigator.clipboard.writeText(text); + showCopySuccess(button); + return true; + } catch (err) { + console.error('Failed to copy:', err); + return false; + } + } + + /** + * Show copy success visual feedback + */ + function showCopySuccess(button) { + const copyIcon = button.querySelector('.icon-copy'); + const checkIcon = button.querySelector('.icon-check'); + + if (copyIcon && checkIcon) { + copyIcon.classList.add('hidden'); + checkIcon.classList.remove('hidden'); + + setTimeout(() => { + copyIcon.classList.remove('hidden'); + checkIcon.classList.add('hidden'); + }, 2000); + } + } + + /** + * Toggle password visibility + */ + function togglePasswordVisibility(button, passwordSpan, password) { + const isHidden = passwordSpan.dataset.hidden === 'true'; + const eyeOpen = button.querySelector('.icon-eye-open'); + const eyeClosed = button.querySelector('.icon-eye-closed'); + + if (isHidden) { + passwordSpan.textContent = password; + passwordSpan.dataset.hidden = 'false'; + button.setAttribute('aria-pressed', 'true'); + eyeOpen?.classList.add('hidden'); + eyeClosed?.classList.remove('hidden'); + } else { + passwordSpan.textContent = '*'.repeat(Math.min(password.length, 12)); + passwordSpan.dataset.hidden = 'true'; + button.setAttribute('aria-pressed', 'false'); + eyeOpen?.classList.remove('hidden'); + eyeClosed?.classList.add('hidden'); + } + } + + // ============================================ + // COMPONENTS - UI component functions + // ============================================ + + /** + * Create a copy button for any text + */ + function createCopyButton(textToCopy) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50'; + button.setAttribute('aria-label', 'Copy to clipboard'); + button.innerHTML = ` + ${Icons.copy('w-4 h-4 text-gray-500 hover:text-brand transition-colors icon-copy')} + ${Icons.check('w-4 h-4 text-brand icon-check hidden')} + `; + + button.addEventListener('click', () => copyToClipboard(textToCopy, button)); + return button; + } + + /** + * Create password field with toggle and copy buttons + */ + function createPasswordField(password) { + const container = document.createElement('div'); + container.className = 'flex items-center gap-1'; + + // Password display + const passwordSpan = document.createElement('span'); + passwordSpan.className = 'font-mono text-sm select-all text-gray-300'; + passwordSpan.textContent = '*'.repeat(Math.min(password.length, 12)); + passwordSpan.dataset.password = password; + passwordSpan.dataset.hidden = 'true'; + + // Toggle visibility button + const toggleBtn = document.createElement('button'); + toggleBtn.type = 'button'; + toggleBtn.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50'; + toggleBtn.setAttribute('aria-label', 'Toggle password visibility'); + toggleBtn.setAttribute('aria-pressed', 'false'); + toggleBtn.innerHTML = ` + ${Icons.eyeOpen('w-4 h-4 text-gray-500 hover:text-brand transition-colors icon-eye-open')} + ${Icons.eyeClosed('w-4 h-4 text-gray-500 hover:text-brand transition-colors icon-eye-closed hidden')} + `; + + toggleBtn.addEventListener('click', () => { + togglePasswordVisibility(toggleBtn, passwordSpan, password); + }); + + // Copy button (reusing the component) + const copyBtn = createCopyButton(password); + + container.appendChild(passwordSpan); + container.appendChild(toggleBtn); + container.appendChild(copyBtn); + + return container; + } + + /** + * Create a credential row with label and value + */ + function createCredentialRow(label, value, isSecret) { + const row = document.createElement('div'); + row.className = 'flex justify-between items-center'; + + const labelSpan = document.createElement('span'); + labelSpan.className = 'text-gray-500 text-sm'; + labelSpan.textContent = `${label}:`; + row.appendChild(labelSpan); + + if (isSecret) { + row.appendChild(createPasswordField(value)); + } else { + const valueContainer = document.createElement('div'); + valueContainer.className = 'flex items-center gap-1'; + + const valueSpan = document.createElement('span'); + valueSpan.className = 'font-mono text-sm select-all text-gray-300'; + valueSpan.textContent = value; + + valueContainer.appendChild(valueSpan); + valueContainer.appendChild(createCopyButton(value)); + row.appendChild(valueContainer); + } + + return row; + } + + /** + * Create credentials section for a service card + */ + function createCredentialsSection(creds) { + const section = document.createElement('div'); + section.className = 'mt-4 pt-4 border-t border-surface-400 space-y-2'; + + if (creds.note) { + const noteP = document.createElement('p'); + noteP.className = 'text-sm text-gray-500 italic'; + noteP.textContent = creds.note; + section.appendChild(noteP); + return section; + } + + if (creds.username) { + section.appendChild(createCredentialRow('Username', creds.username, false)); + } + if (creds.password) { + section.appendChild(createCredentialRow('Password', creds.password, true)); + } + if (creds.api_key) { + section.appendChild(createCredentialRow('API Key', creds.api_key, true)); + } + + return section; + } + + /** + * Create extra info section (internal URLs, etc.) + */ + function createExtraSection(extra) { + const items = []; + + if (extra.internal_api) { + items.push(`Internal: ${escapeHtml(extra.internal_api)}`); + } + if (extra.internal_url) { + items.push(`Internal: ${escapeHtml(extra.internal_url)}`); + } + if (extra.workers) { + items.push(`Workers: ${escapeHtml(extra.workers)}`); + } + if (extra.recommendation) { + items.push(`${escapeHtml(extra.recommendation)}`); + } + + if (items.length === 0) return null; + + const container = document.createElement('div'); + container.className = 'mt-2 flex flex-wrap gap-2'; + container.innerHTML = items.join(''); + return container; + } + + /** + * Create service card header + */ + function createCardHeader(metadata, serviceData) { + const header = document.createElement('div'); + header.className = 'flex items-start gap-4'; + + // Icon + const iconDiv = document.createElement('div'); + iconDiv.className = `${metadata.color} w-11 h-11 rounded-lg flex items-center justify-center text-white font-bold text-sm flex-shrink-0 shadow-lg`; + iconDiv.textContent = metadata.icon; + + // Content + const content = document.createElement('div'); + content.className = 'flex-1 min-w-0'; + + const title = document.createElement('h3'); + title.className = 'font-semibold text-white'; + title.textContent = metadata.name; + + const desc = document.createElement('p'); + desc.className = 'text-sm text-gray-500 mb-2'; + desc.textContent = metadata.description; + + content.appendChild(title); + content.appendChild(desc); + + // Link or internal service indicator + if (serviceData.hostname) { + const link = document.createElement('a'); + link.href = `https://${serviceData.hostname}`; + link.target = '_blank'; + link.rel = 'noopener'; + link.className = 'text-brand hover:text-brand-400 text-sm font-medium inline-flex items-center gap-1 group transition-colors'; + link.innerHTML = ` + ${escapeHtml(serviceData.hostname)} + ${Icons.externalLink('w-3 h-3 group-hover:translate-x-0.5 transition-transform')} + `; + content.appendChild(link); + } else { + const internalSpan = document.createElement('span'); + internalSpan.className = 'text-sm text-gray-600 italic'; + internalSpan.textContent = 'Internal service'; + content.appendChild(internalSpan); + } + + // Extra info + if (serviceData.extra) { + const extraSection = createExtraSection(serviceData.extra); + if (extraSection) content.appendChild(extraSection); + } + + header.appendChild(iconDiv); + header.appendChild(content); + + return header; + } + + /** + * Render a single service card (no setTimeout hack) + */ + function renderServiceCard(key, serviceData) { + const metadata = SERVICE_METADATA[key] || { + name: key, + description: '', + icon: key.substring(0, 2).toUpperCase(), + color: 'bg-gray-600' + }; + + const card = document.createElement('article'); + card.className = 'bg-surface-100 rounded-xl border border-surface-400 p-5 hover-glow'; + // card.className = 'bg-surface-100 rounded-xl border border-surface-400 p-5 hover:border-brand/30 hover:bg-surface-200 transition-all hover-glow'; + + // Build card using DOM API (no innerHTML + setTimeout hack) + const header = createCardHeader(metadata, serviceData); + card.appendChild(header); + + // Credentials section + if (serviceData.credentials && Object.keys(serviceData.credentials).length > 0) { + const credsSection = createCredentialsSection(serviceData.credentials); + card.appendChild(credsSection); + } + + return card; + } + + // ============================================ + // APP - Initialization and rendering + // ============================================ + + // DOM Elements + const servicesContainer = document.getElementById('services-container'); + const quickstartContainer = document.getElementById('quickstart-container'); + const commandsContainer = document.getElementById('commands-container'); + const domainInfo = document.getElementById('domain-info'); + const errorToast = document.getElementById('error-toast'); + const errorMessage = document.getElementById('error-message'); + + /** + * Inject section icons from JS (replaces inline SVG in HTML) + */ + function injectSectionIcons() { + document.querySelectorAll('[data-section-icon]').forEach(container => { + const iconName = container.dataset.sectionIcon; + if (Icons[iconName]) { + container.innerHTML = Icons[iconName]('w-5 h-5 text-brand'); + } + }); + } + + /** + * Show error toast + */ + function showError(message) { + if (!errorToast || !errorMessage) return; + + errorMessage.textContent = message; + errorToast.classList.remove('hidden'); + + requestAnimationFrame(() => { + errorToast.classList.remove('translate-y-20', 'opacity-0'); + }); + + setTimeout(() => { + errorToast.classList.add('translate-y-20', 'opacity-0'); + setTimeout(() => errorToast.classList.add('hidden'), 300); + }, 5000); + } + + /** + * Render all services + */ + function renderServices(services) { + if (!servicesContainer) return; + servicesContainer.innerHTML = ''; + + if (!services || Object.keys(services).length === 0) { + servicesContainer.innerHTML = ` +
+

No services configured. Run the installer to set up services.

+
+ `; + return; + } + + // Sort all services alphabetically by display name + const sortedKeys = Object.keys(services).sort((a, b) => { + const aName = (SERVICE_METADATA[a]?.name || a).toLowerCase(); + const bName = (SERVICE_METADATA[b]?.name || b).toLowerCase(); + return aName.localeCompare(bName); + }); + + // Use DocumentFragment for better performance + const fragment = document.createDocumentFragment(); + sortedKeys.forEach(key => { + fragment.appendChild(renderServiceCard(key, services[key])); + }); + servicesContainer.appendChild(fragment); + } + + /** + * Render quick start steps + */ + function renderQuickStart(steps) { + if (!quickstartContainer) return; + quickstartContainer.innerHTML = ''; + + if (!steps || steps.length === 0) { + // Default steps if none provided + steps = [ + { step: 1, title: 'Log into n8n', description: 'Use the email you provided during installation' }, + { step: 2, title: 'Create your first workflow', description: 'Start with a Manual Trigger + HTTP Request nodes' }, + { step: 3, title: 'Explore community workflows', description: 'Check imported workflows for 300+ examples' }, + { step: 4, title: 'Monitor your system', description: 'Use Grafana to track performance' } + ]; + } + + const fragment = document.createDocumentFragment(); + steps.forEach(item => { + const stepEl = document.createElement('div'); + stepEl.className = 'flex items-start gap-4 p-4 bg-surface-100 rounded-xl border border-surface-400 hover-glow'; + + stepEl.innerHTML = ` +
+ ${item.step} +
+
+

${escapeHtml(item.title)}

+

${escapeHtml(item.description)}

+
+ `; + + fragment.appendChild(stepEl); + }); + quickstartContainer.appendChild(fragment); + } + + /** + * Render make commands + */ + function renderCommands() { + if (!commandsContainer) return; + commandsContainer.innerHTML = ''; + + const grid = document.createElement('div'); + grid.className = 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'; + + COMMANDS.forEach(item => { + const cmdEl = document.createElement('div'); + cmdEl.className = 'flex items-start gap-3 p-3 rounded-lg bg-surface-200/50 border border-surface-400 hover:border-brand/30 transition-all'; + + // Command content + const content = document.createElement('div'); + content.className = 'flex flex-col gap-1 flex-1 min-w-0'; + content.innerHTML = ` + ${escapeHtml(item.cmd)} + ${escapeHtml(item.desc)} + `; + + // Copy button + const copyBtn = createCopyButton(item.cmd); + copyBtn.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50 flex-shrink-0'; + + cmdEl.appendChild(content); + cmdEl.appendChild(copyBtn); + grid.appendChild(cmdEl); + }); + + commandsContainer.appendChild(grid); + } + + /** + * Render error state in services container + */ + function renderServicesError() { + if (!servicesContainer) return; + + servicesContainer.innerHTML = ` + + `; + } + /** * Load data and render page */ async function init() { + // Inject section icons + injectSectionIcons(); + // Always render commands (static content) renderCommands(); @@ -634,12 +855,14 @@ const data = await response.json(); // Update domain info - if (data.domain) { - domainInfo.textContent = `Domain: ${data.domain}`; - } - if (data.generated_at) { - const date = new Date(data.generated_at); - domainInfo.textContent += ` | Generated: ${date.toLocaleString()}`; + if (domainInfo) { + if (data.domain) { + domainInfo.textContent = `Domain: ${data.domain}`; + } + if (data.generated_at) { + const date = new Date(data.generated_at); + domainInfo.textContent += ` | Generated: ${date.toLocaleString()}`; + } } // Render services @@ -652,19 +875,14 @@ console.error('Error loading data:', error); // Show error in UI - servicesContainer.innerHTML = ` -
- - - -

Unable to load service data

-

Make sure the installation completed successfully and data.json was generated.

-
- `; + renderServicesError(); // Still render default quick start renderQuickStart(null); } + + // Initialize cinematic animations (entrance, confetti, card tilt) + CinematicAnimations.init(); } // Initialize when DOM is ready diff --git a/welcome/index.html b/welcome/index.html index e3f7eb1..7984075 100644 --- a/welcome/index.html +++ b/welcome/index.html @@ -5,6 +5,8 @@ Welcome | n8n-install + + - +
- + System Online

- Welcome to n8n-install + Welcome to n8n-install

Your self-hosted automation platform is ready to use @@ -90,11 +144,7 @@

-
- - - -
+

Your Services

@@ -105,14 +155,12 @@
+ +
-
- - - -
+

Quick Start

@@ -122,17 +170,15 @@
+ +
-
- - - -
+

Keeping Up to Date

-
+

Keep your services up to date with the latest features and security patches:

make update @@ -141,56 +187,52 @@
+ +
-
- - - -
+

Useful Commands

-
+
+ +
-
- - - -
+

Documentation

+ + -