import './bootstrap'; import { registerLadillConfirmStore, registerLadillModalHelpers } from './ladill-modals'; import { registerLadillSearchShortcut } from './ladill-search-shortcut'; import { registerLadillDomainPurchase } from './ladill-domain-purchase'; registerLadillModalHelpers(); registerLadillSearchShortcut(); registerLadillDomainPurchase(); import Alpine from 'alpinejs'; import { registerLadillClipboard } from './ladill-clipboard'; import collapse from '@alpinejs/collapse'; Alpine.plugin(collapse); registerLadillClipboard(Alpine); Alpine.data('notificationDropdown', (config = {}) => ({ open: false, loading: false, notifications: [], unreadCount: 0, unreadUrl: config.unreadUrl || '/notifications/unread', markReadUrl: config.markReadUrl || '/notifications/__ID__/read', markAllReadUrl: config.markAllReadUrl || '/notifications/mark-all-read', indexUrl: config.indexUrl || '/notifications', csrfToken: config.csrfToken || document.querySelector('meta[name="csrf-token"]')?.content || '', init() { this.fetchUnread(); setInterval(() => this.fetchUnread(), 60000); }, async fetchUnread() { try { const res = await fetch(this.unreadUrl, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, }); const data = await res.json(); this.notifications = data.notifications || []; this.unreadCount = data.unread_count || 0; } catch (e) { console.error('Failed to fetch notifications', e); } }, toggle() { this.open = !this.open; if (this.open) { this.loading = true; this.fetchUnread().finally(() => { this.loading = false; }); } }, async markRead(id) { const url = this.markReadUrl.replace('__ID__', id); try { await fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': this.csrfToken, 'X-Requested-With': 'XMLHttpRequest', }, }); this.notifications = this.notifications.filter(n => n.id !== id); this.unreadCount = Math.max(0, this.unreadCount - 1); } catch (e) { console.error('Failed to mark notification as read', e); } }, async markAllRead() { try { await fetch(this.markAllReadUrl, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-CSRF-TOKEN': this.csrfToken, 'X-Requested-With': 'XMLHttpRequest', }, }); this.notifications = []; this.unreadCount = 0; } catch (e) { console.error('Failed to mark all notifications as read', e); } }, getIconBg(icon) { const map = { domain: 'bg-emerald-50', hosting: 'bg-violet-50', email: 'bg-pink-50', billing: 'bg-amber-50', success: 'bg-green-50' }; return map[icon] || 'bg-slate-100'; }, getIconColor(icon) { const map = { domain: 'text-emerald-600', hosting: 'text-violet-600', email: 'text-pink-600', billing: 'text-amber-600', success: 'text-green-600' }; return map[icon] || 'text-slate-500'; }, })); // Afia — Ladill in-app AI assistant (slide-over chat). Opened via the topbar AI button // which dispatches a window 'afia-open' event. Greeting/suggestions are passed from Blade. Alpine.data('afia', (config = {}) => ({ open: false, input: '', loading: false, messages: [ { role: 'assistant', text: config.greeting || "Hi, I'm Afia 👋 How can I help?" }, ], suggestions: config.suggestions || [], init() { window.addEventListener('afia-open', () => { this.open = true; this.$nextTick(() => this.$refs.input && this.$refs.input.focus()); }); }, close() { this.open = false; }, useSuggestion(s) { this.input = s; this.send(); }, scrollDown() { this.$nextTick(() => { const el = this.$refs.scroll; if (el) el.scrollTop = el.scrollHeight; }); }, async send() { const text = this.input.trim(); if (!text || this.loading) return; const history = this.messages.map((m) => ({ role: m.role, text: m.text })); this.messages.push({ role: 'user', text }); this.input = ''; this.loading = true; this.scrollDown(); try { const res = await fetch(config.chatUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': config.csrf, 'Accept': 'application/json' }, body: JSON.stringify({ message: text, history }), }); const data = await res.json(); this.messages.push({ role: 'assistant', text: data.reply || data.message || 'Sorry, I could not respond.' }); } catch (e) { this.messages.push({ role: 'assistant', text: 'Network error — please try again.' }); } this.loading = false; this.scrollDown(); }, })); Alpine.data('topbarSearch', (config = {}) => ({ query: config.initialQuery || '', results: Array.isArray(config.initialResults) ? config.initialResults : [], open: !!config.openOnInit, loading: false, active: 0, _abort: null, searchUrl: config.searchUrl || '/search', init() { if (config.autoFocus) { this.$nextTick(() => this.$refs.input?.focus()); } }, onFocus() { if (this.results.length > 0 || this.query.trim().length >= 2) this.open = true; }, async search() { const q = this.query.trim(); if (q.length < 2) { this.results = []; this.open = false; return; } this.loading = true; this.open = true; this.active = 0; if (this._abort) this._abort.abort(); this._abort = new AbortController(); try { const res = await fetch(`${this.searchUrl}?q=${encodeURIComponent(q)}`, { signal: this._abort.signal, headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, }); const data = await res.json(); this.results = data.results || []; } catch (e) { if (e.name !== 'AbortError') this.results = []; } finally { this.loading = false; } }, moveDown() { if (this.active < this.results.length - 1) this.active++; }, moveUp() { if (this.active > 0) this.active--; }, go() { if (this.results[this.active]) window.location.href = this.results[this.active].url; }, })); window.serverOrderForm = (products = {}, billingCycleLabels = {}, initial = {}) => ({ products, billingCycleLabels, steps: [{ label: 'Select Plan' }, { label: 'Configure' }, { label: 'Review' }], step: Number(initial.step ?? 1), selectedProductId: initial.selectedProductId ? String(initial.selectedProductId) : '', selectedProduct: null, serverName: String(initial.serverName ?? ''), billingCycle: String(initial.billingCycle ?? 'monthly'), selections: { region: '', image: '', license: '', application: '', default_user: '', additional_ip: '', private_networking: '', storage_type: '', object_storage: '', }, serverPassword: '', serverPasswordConfirmation: '', submitting: false, error: '', clone(value) { return JSON.parse(JSON.stringify(value ?? null)); }, init() { if (this.selectedProductId) { this.applyProduct(this.selectedProductId, true); } }, applyProduct(productId, preserveState = false) { const product = this.products[String(productId)] || null; if (!product) { return; } this.selectedProductId = String(productId); this.selectedProduct = this.clone(product); const defaults = this.clone(product.defaults || {}); const initialSelections = preserveState ? this.clone(initial.selections || {}) : {}; this.selections = { ...defaults, ...initialSelections, }; if (!preserveState) { this.serverName = ''; this.billingCycle = 'monthly'; this.serverPassword = ''; this.serverPasswordConfirmation = ''; } if (!Object.prototype.hasOwnProperty.call(this.billingCycleLabels, this.billingCycle)) { this.billingCycle = 'monthly'; } this.syncDefaultUser(); this.syncLicenseSelection(); this.error = ''; }, selectProduct(productId) { this.applyProduct(productId, false); }, nextStep() { if (this.step === 1 && !this.selectedProductId) { return; } if (this.step === 2 && !this.serverName.trim()) { return; } if (this.step < this.steps.length) { this.step += 1; } }, catalog(key) { return this.selectedProduct?.catalog?.[key] || []; }, findOption(options, value) { return (options || []).find((option) => option.value === value) || null; }, optionAmount(option) { return Number(option?.prices?.[this.billingCycle] || 0); }, optionLabel(option) { if (!option) { return ''; } const amount = this.optionAmount(option); return amount > 0 ? `${option.label} (+${this.formatMoney(amount)})` : option.label; }, selectionLabel(catalogKey, value) { const option = this.findOption(this.catalog(catalogKey), value); return option?.label || value || '-'; }, currentImage() { return this.findOption(this.catalog('images'), this.selections.image); }, imageSupportsManagedStack(image = null) { const currentImage = image || this.currentImage(); const osFamily = String(currentImage?.os_family || this.selectedProduct?.catalog?.default_image_os_family || 'linux').toLowerCase(); const imageLabel = String(currentImage?.label || '').toLowerCase(); const installLater = Boolean(currentImage?.is_install_later); if (installLater || osFamily.startsWith('win')) { return false; } if (['cpanel', 'plesk', 'whm'].some((needle) => imageLabel.includes(needle))) { return false; } return ['ubuntu 22.04', 'ubuntu 24.04', 'debian 12', 'bookworm', 'almalinux', 'alma linux', 'alma', 'rocky linux', 'rocky', 'fedora', 'opensuse leap', 'open suse leap', 'opensuse', 'leap', 'centos'] .some((needle) => imageLabel.includes(needle)); }, bundledPanelFromImage(image = null) { const imageLabel = String((image || this.currentImage())?.label || '').toLowerCase(); if (imageLabel.includes('cpanel') || imageLabel.includes('whm')) { return 'cpanel'; } if (imageLabel.includes('plesk')) { return 'plesk'; } return null; }, availableLicenseOptions() { const image = this.currentImage(); const installLater = Boolean(image?.is_install_later); const bundledPanel = this.bundledPanelFromImage(image); return this.catalog('licenses').filter((option) => { if (installLater) { return option.value === 'none'; } if (bundledPanel && option.value === 'none') { return false; } const optionLicense = String(option.license || '').toLowerCase(); if (bundledPanel === 'cpanel' && optionLicense.startsWith('plesk')) { return false; } if (bundledPanel === 'plesk' && optionLicense.startsWith('cpanel')) { return false; } if (option.panel !== 'ladill') { return true; } return this.imageSupportsManagedStack(image); }); }, currentLicenseOption() { return this.findOption(this.availableLicenseOptions(), this.selections.license) || this.findOption(this.catalog('licenses'), this.selections.license); }, currentPanelSnapshot() { const license = this.currentLicenseOption(); const image = this.currentImage(); const managedCandidate = this.imageSupportsManagedStack(image); if (license?.panel === 'ladill') { return { label: license.label || 'Ladill Server Manager', primary: managedCandidate ? 'Ladill Server Manager will provision this server with Ladill-managed controls on the selected supported Linux image.' : 'Ladill Server Manager is not available for the selected image.', secondary: managedCandidate ? 'After provisioning finishes, Server Manager can handle power, status, files, databases, domains, PHP, SSL, cron, and logs.' : 'Choose a plain supported Linux image without cPanel or Plesk to use Ladill-managed hosting features.', futureCapabilities: managedCandidate ? ['Files', 'Databases', 'Domains', 'PHP', 'SSL', 'Cron', 'Logs'] : [], toneClasses: managedCandidate ? 'border-amber-200 bg-amber-50 text-amber-900' : 'border-slate-200 bg-slate-50 text-slate-700', }; } if (license?.license) { return { label: license.label || 'Server Panel', primary: `${license.label} will run inside the server as the primary control panel.`, secondary: 'Ladill will keep the order and infrastructure lifecycle, but site and service management will happen in the installed panel.', futureCapabilities: [], toneClasses: 'border-blue-200 bg-blue-50 text-blue-900', }; } return { label: license?.label || 'Remote Login Only', primary: 'This selection provisions the server without a Ladill-managed or commercial control panel.', secondary: 'You will manage the machine directly over SSH or RDP until a panel or managed runtime is installed separately.', futureCapabilities: [], toneClasses: 'border-gray-200 bg-gray-50 text-gray-700', }; }, defaultUserOptions() { const defaultFamily = this.selectedProduct?.catalog?.default_image_os_family || 'linux'; const osFamily = this.currentImage()?.os_family || defaultFamily; return osFamily.startsWith('win') ? this.catalog('windows_default_users') : this.catalog('linux_default_users'); }, syncDefaultUser() { const options = this.defaultUserOptions(); if (!options.some((option) => option.value === this.selections.default_user)) { this.selections.default_user = options[0]?.value || ''; } }, syncLicenseSelection() { const options = this.availableLicenseOptions(); if (!options.some((option) => option.value === this.selections.license)) { this.selections.license = options[0]?.value || 'none'; } }, handleImageChange() { this.syncDefaultUser(); this.syncLicenseSelection(); }, basePrice() { return Number(this.selectedProduct?.prices?.[this.billingCycle] || 0); }, setupFee() { return Number(this.selectedProduct?.setup_fees?.[this.billingCycle] || 0); }, addOnLineItems() { const items = []; const setupFee = this.setupFee(); if (setupFee > 0) { items.push({ key: 'setup_fee', label: 'One-time setup fee', amount: setupFee, }); } const keys = ['image', 'region', 'license', 'application', 'additional_ip', 'private_networking', 'storage_type', 'object_storage']; const catalogKeys = { image: 'images', region: 'regions', license: 'licenses', application: 'applications', additional_ip: 'additional_ip', private_networking: 'private_networking', storage_type: 'storage_types', object_storage: 'object_storage', }; keys.forEach((key) => { const option = this.findOption(this.catalog(catalogKeys[key]), this.selections[key]); const amount = this.optionAmount(option); if (amount > 0) { items.push({ key, label: option.label, amount, }); } }); return items; }, currentTotal() { return this.basePrice() + this.addOnLineItems().reduce((sum, item) => sum + item.amount, 0); }, cycleMonths() { return { monthly: 1, quarterly: 3, semiannual: 6, yearly: 12, }[this.billingCycle] || 1; }, monthlyEquivalent() { return this.currentTotal() / this.cycleMonths(); }, formatMoney(amount, currency = null) { if (amount === null || amount === undefined) { return '-'; } const code = currency || this.selectedProduct?.currency || 'GHS'; return `${code} ${Number(amount).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2, })}`; }, canSubmit() { return Boolean( this.selectedProductId && this.serverName.trim() && this.serverPassword && this.serverPassword === this.serverPasswordConfirmation && this.serverPassword.length >= 8 ); }, submitOrder() { if (!this.canSubmit()) { this.error = 'Complete the required fields and make sure the passwords match.'; return; } this.submitting = true; this.error = ''; this.$refs.form.submit(); }, }); // Wallet balance peek for the avatar dropdown. Alpine.data('walletWidget', (config = {}) => ({ display: '…', async load() { if (! config.url) { this.display = 'View wallet'; return; } try { const res = await fetch(config.url, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); const data = await res.json(); this.display = data.available ? data.formatted : 'View wallet'; } catch (e) { this.display = 'View wallet'; } }, })); window.Alpine = Alpine; registerLadillConfirmStore(Alpine); Alpine.start();