🧱 update CI workflow and remove alpinejs frontend files
This commit is contained in:
5
frontend/public/alpine.min.js
vendored
Normal file
5
frontend/public/alpine.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/public/config.js
Normal file
1
frontend/public/config.js
Normal file
@@ -0,0 +1 @@
|
||||
window.APP_CONFIG = {"api_url":"http://localhost:8080","port":8090};
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 699 B |
14
frontend/public/index.html
Normal file
14
frontend/public/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Handheld Emulation Devices Manager</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="config.js"></script>
|
||||
<script src="main.js" defer></script>
|
||||
<script src="alpine.min.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
449
frontend/public/main.js
Normal file
449
frontend/public/main.js
Normal file
@@ -0,0 +1,449 @@
|
||||
// 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">
|
||||
×
|
||||
</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">
|
||||
×
|
||||
</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;
|
||||
}
|
||||
}));
|
||||
});
|
||||
432
frontend/public/style.css
Normal file
432
frontend/public/style.css
Normal file
@@ -0,0 +1,432 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--secondary: #6b7280;
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--dark: #1f2937;
|
||||
--light: #f3f4f6;
|
||||
--border: #e5e7eb;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 25px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, #667eea 100%);
|
||||
color: white;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding: 20px;
|
||||
background: var(--light);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: var(--warning);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 10px 15px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.devices-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.devices-table thead {
|
||||
background: var(--light);
|
||||
border-bottom: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.devices-table th {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.devices-table tbody tr {
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.devices-table tbody tr:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.devices-table td {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.device-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 0.9em;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--secondary);
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.modal::backdrop {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.modal:popover-open {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--light);
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2em;
|
||||
cursor: pointer;
|
||||
color: var(--secondary);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.device-details {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
padding: 15px;
|
||||
background: var(--light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: var(--secondary);
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.detail-item span:last-child {
|
||||
font-size: 1.1em;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.actions-modal {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Toast Notification */
|
||||
.toast {
|
||||
margin: 0;
|
||||
padding: 15px 20px;
|
||||
border: none;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: var(--success);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 2000;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.toast:popover-open {
|
||||
display: block;
|
||||
animation: toastIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes toastIn {
|
||||
from {
|
||||
transform: translateY(100px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
header h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.devices-table {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.devices-table th,
|
||||
.devices-table td {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.details-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user