Files
LuaMicroservices/frontend/public/main.js
Christian van Dijk a6daab7961
Some checks failed
CI / Lint (push) Successful in 21s
CI / Helm Lint (push) Successful in 5s
CI / Build (push) Failing after 43s
CI / Test (push) Has been skipped
🧱 update CI workflow and remove alpinejs frontend files
2026-02-23 15:46:31 +01:00

450 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Inject the HTML before Alpine initializes its data components
if (!document.querySelector('.container')) {
document.body.innerHTML = `
<div class="container" x-data="deviceManager" x-init="loadDevices(); connectWebSocket();" x-cloak>
<header>
<h1>🎮 Handheld Emulation Devices</h1>
<p class="subtitle">Manage your collection of retro handheld devices</p>
</header>
<div class="controls">
<button class="btn btn-primary" @click="addNewDevice()">
Add New Device
</button>
<input
type="text"
x-model="searchQuery"
placeholder="Search devices..."
class="search-input"
/>
</div>
<!-- Create/Edit Form Modal -->
<div
id="createFormPopover"
popover
class="modal"
@toggle="showCreateForm = $event.newState === 'open'; if ($event.newState === 'closed') editingDevice = null;"
>
<div class="modal-content" @click.stop>
<div class="modal-header">
<h2 x-text="editingDevice ? 'Edit Device' : 'Add New Device'"></h2>
<button class="close-btn" @click="showCreateForm = false">
&times;
</button>
</div>
<form @submit.prevent="saveDevice()" class="form">
<div class="form-group">
<label>Device Name *</label>
<input type="text" x-model="form.name" required />
</div>
<div class="form-row">
<div class="form-group">
<label>Manufacturer *</label>
<input type="text" x-model="form.manufacturer" required />
</div>
<div class="form-group">
<label>Release Year</label>
<input type="number" x-model="form.release_year" />
</div>
</div>
<div class="form-group">
<label>CPU</label>
<input type="text" x-model="form.cpu" />
</div>
<div class="form-row">
<div class="form-group">
<label>RAM (MB)</label>
<input type="number" x-model="form.ram_mb" />
</div>
<div class="form-group">
<label>Storage (MB)</label>
<input type="number" x-model="form.storage_mb" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Display Size</label>
<input
type="text"
x-model="form.display_size"
placeholder="e.g., 3.5 inch"
/>
</div>
<div class="form-group">
<label>Battery (Hours)</label>
<input type="number" x-model="form.battery_hours" step="0.5" />
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-success">Save Device</button>
<button
type="button"
class="btn btn-secondary"
@click="showCreateForm = false"
>
Cancel
</button>
</div>
</form>
</div>
</div>
<!-- Devices Table -->
<div class="table-container">
<table class="devices-table">
<thead>
<tr>
<th>Device Name</th>
<th>Manufacturer</th>
<th>Year</th>
<th>CPU</th>
<th>RAM</th>
<th>Display</th>
<th>Battery</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<template x-for="device in filteredDevices" :key="device.id">
<tr>
<td class="device-name" @click="viewDevice(device)">
<strong x-text="device.name"></strong>
</td>
<td x-text="device.manufacturer"></td>
<td x-text="device.release_year || '-'"></td>
<td class="small" x-text="device.cpu || '-'"></td>
<td x-text="device.ram_mb ? device.ram_mb + ' MB' : '-'"></td>
<td x-text="device.display_size || '-'"></td>
<td
x-text="device.battery_hours ? device.battery_hours + 'h' : '-'"
></td>
<td class="actions">
<button
class="btn-icon"
@click="editDevice(device)"
title="Edit"
>
✏️
</button>
<button
class="btn-icon btn-danger"
@click="deleteDevice(device.id)"
title="Delete"
>
🗑️
</button>
</td>
</tr>
</template>
</tbody>
</table>
<div x-show="filteredDevices.length === 0" class="empty-state">
<p>No devices found. Try adding one!</p>
</div>
</div>
<!-- Device Details Modal -->
<div
id="detailsModalPopover"
popover
class="modal"
@toggle="showDetailsModal = $event.newState === 'open'"
>
<div class="modal-content" @click.stop>
<div class="modal-header">
<h2 x-text="selectedDevice?.name || 'Device Details'"></h2>
<button class="close-btn" @click="showDetailsModal = false">
&times;
</button>
</div>
<div class="device-details" x-show="selectedDevice">
<div class="details-grid">
<div class="detail-item">
<span class="label">Manufacturer:</span>
<span x-text="selectedDevice?.manufacturer"></span>
</div>
<div class="detail-item">
<span class="label">Release Year:</span>
<span x-text="selectedDevice?.release_year || 'N/A'"></span>
</div>
<div class="detail-item">
<span class="label">CPU:</span>
<span x-text="selectedDevice?.cpu || 'N/A'"></span>
</div>
<div class="detail-item">
<span class="label">RAM:</span>
<span
x-text="selectedDevice?.ram_mb ? selectedDevice.ram_mb + ' MB' : 'N/A'"
></span>
</div>
<div class="detail-item">
<span class="label">Storage:</span>
<span
x-text="selectedDevice?.storage_mb ? selectedDevice.storage_mb + ' MB' : 'N/A'"
></span>
</div>
<div class="detail-item">
<span class="label">Display:</span>
<span x-text="selectedDevice?.display_size || 'N/A'"></span>
</div>
<div class="detail-item">
<span class="label">Battery Life:</span>
<span
x-text="selectedDevice?.battery_hours ? selectedDevice.battery_hours + ' hours' : 'N/A'"
></span>
</div>
<div class="detail-item">
<span class="label">Average Rating:</span>
<span
x-text="selectedDevice?.average_rating ? selectedDevice.average_rating.toFixed(1) + ' ⭐' : 'No ratings'"
></span>
</div>
</div>
<div class="actions-modal">
<button
class="btn btn-warning"
@click="editDevice(selectedDevice)"
>
Edit
</button>
<button
class="btn btn-secondary"
@click="showDetailsModal = false"
>
Close
</button>
</div>
</div>
</div>
</div>
<!-- Toast Notification -->
<div
id="toastPopover"
popover="manual"
class="toast"
x-text="toastMessage"
></div>
</div>
`;
}
document.addEventListener('alpine:init', () => {
Alpine.data('deviceManager', () => ({
devices: [],
searchQuery: '',
showCreateForm: false,
showDetailsModal: false,
showToast: false,
toastMessage: '',
editingDevice: null,
selectedDevice: null,
// Use config from server or default
apiBase: window.APP_CONFIG?.api_url || '',
init() {
this.$watch('showCreateForm', value => {
const popover = document.getElementById('createFormPopover');
if (value) {
try { popover.showPopover(); } catch(e) {}
} else {
try { popover.hidePopover(); } catch(e) {}
}
});
this.$watch('showDetailsModal', value => {
const popover = document.getElementById('detailsModalPopover');
if (value) {
try { popover.showPopover(); } catch(e) {}
} else {
try { popover.hidePopover(); } catch(e) {}
}
});
this.$watch('showToast', value => {
const popover = document.getElementById('toastPopover');
if (value) {
try { popover.showPopover(); } catch(e) {}
} else {
try { popover.hidePopover(); } catch(e) {}
}
});
},
form: {
name: '',
manufacturer: '',
release_year: null,
cpu: '',
ram_mb: null,
storage_mb: null,
display_size: '',
battery_hours: null
},
get filteredDevices() {
return this.devices.filter(device =>
device.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
device.manufacturer.toLowerCase().includes(this.searchQuery.toLowerCase())
);
},
async loadDevices() {
try {
const response = await fetch(this.apiBase + '/devices');
const data = await response.json();
console.log('Devices data:', data);
this.devices = Array.isArray(data.data) ? data.data : (Array.isArray(data) ? data : []);
} catch (error) {
this.showNotification('Failed to load devices');
console.error(error);
}
},
connectWebSocket() {
const wsUrl = this.apiBase.replace('http', 'ws');
console.log('Connecting to WebSocket:', wsUrl);
const socket = new WebSocket(wsUrl);
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('WS Message received:', data);
if (data.event_type === 'DevicePublished') {
this.showNotification(`New device: ${data.device_name}`);
this.loadDevices(); // Refresh list
} else if (data.event_type === 'DeviceUpdated') {
this.showNotification(`Device updated: ${data.device_name}`);
this.loadDevices(); // Refresh list
} else if (data.event_type === 'DeviceDeleted') {
this.showNotification(`Device removed: ${data.device_name}`);
// Optimistically remove from local list or just refresh
this.devices = this.devices.filter(d => d.id != data.device_id);
if (this.selectedDevice && this.selectedDevice.id == data.device_id) {
this.showDetailsModal = false;
}
}
};
socket.onclose = () => {
console.log('WS disconnected, retrying in 5s...');
setTimeout(() => this.connectWebSocket(), 5000);
};
socket.onerror = (error) => {
console.error('WS Error:', error);
};
},
async saveDevice() {
try {
const url = this.editingDevice
? `${this.apiBase}/devices/${this.editingDevice.id}`
: `${this.apiBase}/devices`;
const method = this.editingDevice ? 'PUT' : 'POST';
// Filter out null/undefined values and only send necessary data
const payload = {
name: this.form.name,
manufacturer: this.form.manufacturer,
release_year: this.form.release_year ? parseInt(this.form.release_year) : null,
cpu: this.form.cpu || null,
ram_mb: this.form.ram_mb ? parseInt(this.form.ram_mb) : null,
storage_mb: this.form.storage_mb ? parseInt(this.form.storage_mb) : null,
display_size: this.form.display_size || null,
battery_hours: this.form.battery_hours ? parseFloat(this.form.battery_hours) : null
};
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.ok) {
await this.loadDevices();
this.resetForm();
this.showCreateForm = false;
this.showNotification(
this.editingDevice ? 'Device updated successfully!' : 'Device created successfully!'
);
} else {
const errorData = await response.json().catch(() => ({}));
this.showNotification(`Failed to save device: ${errorData.error || response.statusText}`);
}
} catch (error) {
this.showNotification('Failed to save device: Network error');
console.error(error);
}
},
editDevice(device) {
this.editingDevice = device;
this.form = { ...device };
this.showCreateForm = true;
this.showDetailsModal = false;
},
async deleteDevice(id) {
if (confirm('Are you sure you want to delete this device?')) {
try {
const response = await fetch(`${this.apiBase}/devices/${id}`, {
method: 'DELETE'
});
if (response.ok) {
await this.loadDevices();
this.showNotification('Device deleted successfully!');
} else {
this.showNotification('Failed to delete device');
}
} catch (error) {
this.showNotification('Failed to delete device');
console.error(error);
}
}
},
viewDevice(device) {
this.selectedDevice = device;
this.showDetailsModal = true;
},
resetForm() {
this.form = {
name: '',
manufacturer: '',
release_year: null,
cpu: '',
ram_mb: null,
storage_mb: null,
display_size: '',
battery_hours: null
};
this.editingDevice = null;
},
showNotification(message) {
this.toastMessage = message;
this.showToast = true;
setTimeout(() => {
this.showToast = false;
}, 3000);
},
addNewDevice() {
this.resetForm();
this.showCreateForm = true;
}
}));
});