mirror of
https://github.com/sassanix/Warracker.git
synced 2026-01-06 05:29:39 -06:00
Added Serial Numbers, Fixed CSS, Added Dark Mode
# Changelog ## [0.05.2-beta] - 2024-03-05 ### Added - Multiple serial numbers support for warranties - Users can now add multiple serial numbers per warranty item - Dynamic form fields for adding/removing serial numbers - Database schema updated to support multiple serial numbers - Added settings menu - Added Darkmode ### Changed - Enhanced warranty management interface - Improved form handling for serial numbers - Better organization of warranty details - Optimized database queries with new indexes - Added index for serial numbers lookup - Added index for warranty ID relationships ### Technical - Database schema improvements - New `serial_numbers` table with proper foreign key constraints - Added indexes for better query performance - Implemented cascading deletes for warranty-serial number relationships ### Fixed - Form validation and handling for multiple serial numbers - Database connection management and resource cleanup
This commit is contained in:
29
CHANGELOG.md
Normal file
29
CHANGELOG.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
## [0.05.2-beta] - 2024-03-05
|
||||
|
||||
### Added
|
||||
- Multiple serial numbers support for warranties
|
||||
- Users can now add multiple serial numbers per warranty item
|
||||
- Dynamic form fields for adding/removing serial numbers
|
||||
- Database schema updated to support multiple serial numbers
|
||||
|
||||
### Changed
|
||||
- Enhanced warranty management interface
|
||||
- Improved form handling for serial numbers
|
||||
- Better organization of warranty details
|
||||
- Optimized database queries with new indexes
|
||||
- Added index for serial numbers lookup
|
||||
- Added index for warranty ID relationships
|
||||
|
||||
### Technical
|
||||
- Database schema improvements
|
||||
- New `serial_numbers` table with proper foreign key constraints
|
||||
- Added indexes for better query performance
|
||||
- Implemented cascading deletes for warranty-serial number relationships
|
||||
|
||||
### Fixed
|
||||
- Form validation and handling for multiple serial numbers
|
||||
- Database connection management and resource cleanup
|
||||
|
||||
[0.05.2-beta]: https://github.com/username/warracker/releases/tag/v0.05.2-beta
|
||||
@@ -44,7 +44,7 @@ Warracker is a web-based application that provides a centralized system for mana
|
||||
1. **Clone the Repository:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/sassanix/warracker.git
|
||||
git clone https://github.com/yourusername/warracker.git
|
||||
cd warracker
|
||||
```
|
||||
2. **Start the Application:**
|
||||
@@ -108,9 +108,6 @@ warracker/
|
||||
* Mobile app.
|
||||
* Settings page.
|
||||
* Status page.
|
||||
* Status page.
|
||||
* Tags
|
||||
* Serials
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
|
||||
@@ -103,6 +103,21 @@ def init_db():
|
||||
cur.execute('CREATE INDEX IF NOT EXISTS idx_expiration_date ON warranties(expiration_date)')
|
||||
cur.execute('CREATE INDEX IF NOT EXISTS idx_product_name ON warranties(product_name)')
|
||||
|
||||
# Create serial numbers table
|
||||
cur.execute('''
|
||||
CREATE TABLE IF NOT EXISTS serial_numbers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
warranty_id INTEGER NOT NULL,
|
||||
serial_number VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (warranty_id) REFERENCES warranties(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
|
||||
# Add indexes for serial numbers
|
||||
cur.execute('CREATE INDEX IF NOT EXISTS idx_warranty_id ON serial_numbers(warranty_id)')
|
||||
cur.execute('CREATE INDEX IF NOT EXISTS idx_serial_number ON serial_numbers(serial_number)')
|
||||
|
||||
conn.commit()
|
||||
logger.info("Database initialized successfully")
|
||||
except Exception as e:
|
||||
@@ -131,6 +146,13 @@ def get_warranties():
|
||||
for key, value in warranty_dict.items():
|
||||
if isinstance(value, (datetime, date)):
|
||||
warranty_dict[key] = value.isoformat()
|
||||
|
||||
# Get serial numbers for this warranty
|
||||
warranty_id = warranty_dict['id']
|
||||
cur.execute('SELECT serial_number FROM serial_numbers WHERE warranty_id = %s', (warranty_id,))
|
||||
serial_numbers = [row[0] for row in cur.fetchall()]
|
||||
warranty_dict['serial_numbers'] = serial_numbers
|
||||
|
||||
warranties_list.append(warranty_dict)
|
||||
|
||||
return jsonify(warranties_list)
|
||||
@@ -162,6 +184,7 @@ def add_warranty():
|
||||
# Process the data
|
||||
product_name = request.form['product_name']
|
||||
purchase_date_str = request.form['purchase_date']
|
||||
serial_numbers = request.form.getlist('serial_numbers')
|
||||
|
||||
try:
|
||||
purchase_date = datetime.strptime(purchase_date_str, '%Y-%m-%d')
|
||||
@@ -192,12 +215,23 @@ def add_warranty():
|
||||
# Save to database
|
||||
conn = get_db_connection()
|
||||
with conn.cursor() as cur:
|
||||
# Insert warranty
|
||||
cur.execute('''
|
||||
INSERT INTO warranties (product_name, purchase_date, warranty_years, expiration_date, invoice_path)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
''', (product_name, purchase_date, warranty_years, expiration_date, db_invoice_path))
|
||||
warranty_id = cur.fetchone()[0]
|
||||
|
||||
# Insert serial numbers
|
||||
if serial_numbers:
|
||||
for serial_number in serial_numbers:
|
||||
if serial_number.strip(): # Only insert non-empty serial numbers
|
||||
cur.execute('''
|
||||
INSERT INTO serial_numbers (warranty_id, serial_number)
|
||||
VALUES (%s, %s)
|
||||
''', (warranty_id, serial_number.strip()))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return jsonify({
|
||||
@@ -272,6 +306,7 @@ def update_warranty(warranty_id):
|
||||
# Process the data
|
||||
product_name = request.form['product_name']
|
||||
purchase_date_str = request.form['purchase_date']
|
||||
serial_numbers = request.form.getlist('serial_numbers')
|
||||
|
||||
try:
|
||||
purchase_date = datetime.strptime(purchase_date_str, '%Y-%m-%d')
|
||||
@@ -321,6 +356,19 @@ def update_warranty(warranty_id):
|
||||
''', (product_name, purchase_date, warranty_years, expiration_date,
|
||||
db_invoice_path, warranty_id))
|
||||
|
||||
# Update serial numbers
|
||||
# First, delete existing serial numbers for this warranty
|
||||
cur.execute('DELETE FROM serial_numbers WHERE warranty_id = %s', (warranty_id,))
|
||||
|
||||
# Then insert the new serial numbers
|
||||
if serial_numbers:
|
||||
for serial_number in serial_numbers:
|
||||
if serial_number.strip(): # Only insert non-empty serial numbers
|
||||
cur.execute('''
|
||||
INSERT INTO serial_numbers (warranty_id, serial_number)
|
||||
VALUES (%s, %s)
|
||||
''', (warranty_id, serial_number.strip()))
|
||||
|
||||
conn.commit()
|
||||
return jsonify({"message": "Warranty updated successfully"}), 200
|
||||
|
||||
|
||||
@@ -11,4 +11,16 @@ CREATE TABLE warranties (
|
||||
);
|
||||
|
||||
CREATE INDEX idx_expiration_date ON warranties(expiration_date);
|
||||
CREATE INDEX idx_product_name ON warranties(product_name);
|
||||
CREATE INDEX idx_product_name ON warranties(product_name);
|
||||
|
||||
-- Add serial numbers table
|
||||
CREATE TABLE serial_numbers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
warranty_id INTEGER NOT NULL,
|
||||
serial_number VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (warranty_id) REFERENCES warranties(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_warranty_id ON serial_numbers(warranty_id);
|
||||
CREATE INDEX idx_serial_number ON serial_numbers(serial_number);
|
||||
11
backend/migrations/001_add_serial_numbers.sql
Normal file
11
backend/migrations/001_add_serial_numbers.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Add serial numbers table
|
||||
CREATE TABLE serial_numbers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
warranty_id INTEGER NOT NULL,
|
||||
serial_number VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (warranty_id) REFERENCES warranties(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_warranty_id ON serial_numbers(warranty_id);
|
||||
CREATE INDEX idx_serial_number ON serial_numbers(serial_number);
|
||||
@@ -16,6 +16,20 @@
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<h1>Warranty Tracker</h1>
|
||||
</div>
|
||||
<div class="settings-container">
|
||||
<button id="settingsBtn" class="settings-btn" aria-label="Settings">
|
||||
<i class="fas fa-cog"></i>
|
||||
</button>
|
||||
<div id="settingsMenu" class="settings-menu">
|
||||
<div class="settings-item">
|
||||
<span>Dark Mode</span>
|
||||
<label class="toggle-switch" title="Toggle dark mode">
|
||||
<input type="checkbox" id="darkModeToggle">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -41,6 +55,18 @@
|
||||
<input type="number" id="warrantyYears" name="warranty_years" class="form-control" min="1" max="100" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Serial Numbers</label>
|
||||
<div id="serialNumbersContainer">
|
||||
<div class="serial-number-input">
|
||||
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number">
|
||||
<button type="button" class="btn btn-sm btn-secondary add-serial-number">
|
||||
<i class="fas fa-plus"></i> Add Another
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Invoice/Receipt</label>
|
||||
<div class="file-input-wrapper">
|
||||
@@ -107,6 +133,18 @@
|
||||
<input type="number" id="editWarrantyYears" name="warranty_years" class="form-control" min="1" max="100" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Serial Numbers</label>
|
||||
<div id="editSerialNumbersContainer">
|
||||
<div class="serial-number-input">
|
||||
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number">
|
||||
<button type="button" class="btn btn-sm btn-secondary add-serial-number">
|
||||
<i class="fas fa-plus"></i> Add Another
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Invoice/Receipt</label>
|
||||
<div class="file-input-wrapper">
|
||||
|
||||
@@ -1,5 +1,78 @@
|
||||
// DOM Elements
|
||||
const warrantyForm = document.getElementById('warrantyForm');
|
||||
const settingsBtn = document.getElementById('settingsBtn');
|
||||
const settingsMenu = document.getElementById('settingsMenu');
|
||||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||||
|
||||
// Theme Management
|
||||
function setTheme(isDark) {
|
||||
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||
localStorage.setItem('darkMode', isDark);
|
||||
darkModeToggle.checked = isDark;
|
||||
}
|
||||
|
||||
// Initialize theme based on user preference or system preference
|
||||
function initializeTheme() {
|
||||
const savedTheme = localStorage.getItem('darkMode');
|
||||
|
||||
if (savedTheme !== null) {
|
||||
// Use saved preference
|
||||
setTheme(savedTheme === 'true');
|
||||
} else {
|
||||
// Check for system preference
|
||||
const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setTheme(prefersDarkMode);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme when page loads
|
||||
initializeTheme();
|
||||
|
||||
// Settings menu toggle
|
||||
settingsBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
settingsMenu.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Close settings menu when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!settingsMenu.contains(e.target) && !settingsBtn.contains(e.target)) {
|
||||
settingsMenu.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Dark mode toggle
|
||||
darkModeToggle.addEventListener('change', (e) => {
|
||||
setTheme(e.target.checked);
|
||||
});
|
||||
|
||||
const serialNumbersContainer = document.getElementById('serialNumbersContainer');
|
||||
|
||||
// Add event listener for adding new serial number inputs
|
||||
serialNumbersContainer.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.add-serial-number')) {
|
||||
addSerialNumberInput();
|
||||
}
|
||||
});
|
||||
|
||||
function addSerialNumberInput(container = serialNumbersContainer) {
|
||||
const newInput = document.createElement('div');
|
||||
newInput.className = 'serial-number-input';
|
||||
newInput.innerHTML = `
|
||||
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number">
|
||||
<button type="button" class="btn btn-sm btn-danger remove-serial-number">
|
||||
<i class="fas fa-minus"></i> Remove
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add remove button functionality
|
||||
newInput.querySelector('.remove-serial-number').addEventListener('click', function() {
|
||||
this.parentElement.remove();
|
||||
});
|
||||
|
||||
container.appendChild(newInput);
|
||||
}
|
||||
|
||||
const warrantiesList = document.getElementById('warrantiesList');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
const searchInput = document.getElementById('searchWarranties');
|
||||
@@ -180,6 +253,11 @@ function renderWarranties(filteredWarranties = null) {
|
||||
statusText = `${daysRemaining} days remaining`;
|
||||
}
|
||||
|
||||
// Make sure serial numbers array exists and is valid
|
||||
const validSerialNumbers = Array.isArray(warranty.serial_numbers)
|
||||
? warranty.serial_numbers.filter(sn => sn && typeof sn === 'string' && sn.trim() !== '')
|
||||
: [];
|
||||
|
||||
const cardElement = document.createElement('div');
|
||||
cardElement.className = `warranty-card ${statusClass === 'expired' ? 'expired' : statusClass === 'expiring' ? 'expiring-soon' : ''}`;
|
||||
cardElement.innerHTML = `
|
||||
@@ -199,6 +277,14 @@ function renderWarranties(filteredWarranties = null) {
|
||||
<div>Warranty: <span>${warranty.warranty_years} ${warranty.warranty_years > 1 ? 'years' : 'year'}</span></div>
|
||||
<div>Expires: <span>${formatDate(expirationDate)}</span></div>
|
||||
<span class="warranty-status status-${statusClass}">${statusText}</span>
|
||||
${validSerialNumbers.length > 0 ? `
|
||||
<div class="serial-numbers">
|
||||
<strong>Serial Numbers:</strong>
|
||||
<ul>
|
||||
${validSerialNumbers.map(sn => `<li>${sn}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
${warranty.invoice_path ? `
|
||||
<div>
|
||||
<a href="${warranty.invoice_path}" class="invoice-link" target="_blank">
|
||||
@@ -253,6 +339,18 @@ async function addWarranty(event) {
|
||||
|
||||
const formData = new FormData(warrantyForm);
|
||||
|
||||
// Get all serial numbers and add them to formData
|
||||
const serialNumbers = [];
|
||||
document.querySelectorAll('input[name="serial_numbers[]"]').forEach(input => {
|
||||
if (input.value.trim()) {
|
||||
serialNumbers.push(input.value.trim());
|
||||
}
|
||||
});
|
||||
formData.delete('serial_numbers[]'); // Remove the original array
|
||||
if (serialNumbers.length > 0) {
|
||||
serialNumbers.forEach(sn => formData.append('serial_numbers', sn));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
@@ -271,6 +369,26 @@ async function addWarranty(event) {
|
||||
warrantyForm.reset();
|
||||
fileName.textContent = '';
|
||||
|
||||
// Completely reset serial number inputs
|
||||
serialNumbersContainer.innerHTML = '';
|
||||
|
||||
// Create a fresh initial serial number input
|
||||
const initialInput = document.createElement('div');
|
||||
initialInput.className = 'serial-number-input';
|
||||
initialInput.innerHTML = `
|
||||
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number">
|
||||
<button type="button" class="btn btn-sm btn-secondary add-serial-number">
|
||||
<i class="fas fa-plus"></i> Add Another
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add the event listener for the "Add Another" button
|
||||
initialInput.querySelector('.add-serial-number').addEventListener('click', function() {
|
||||
addSerialNumberInput();
|
||||
});
|
||||
|
||||
serialNumbersContainer.appendChild(initialInput);
|
||||
|
||||
// Reload warranties
|
||||
loadWarranties();
|
||||
} catch (error) {
|
||||
@@ -290,6 +408,57 @@ function openEditModal(warranty) {
|
||||
document.getElementById('editPurchaseDate').value = new Date(warranty.purchase_date).toISOString().split('T')[0];
|
||||
document.getElementById('editWarrantyYears').value = warranty.warranty_years;
|
||||
|
||||
// Clear existing serial number inputs
|
||||
const editSerialNumbersContainer = document.getElementById('editSerialNumbersContainer');
|
||||
editSerialNumbersContainer.innerHTML = '';
|
||||
|
||||
// Make sure serial numbers array exists and is valid
|
||||
const validSerialNumbers = Array.isArray(warranty.serial_numbers)
|
||||
? warranty.serial_numbers.filter(sn => sn && typeof sn === 'string' && sn.trim() !== '')
|
||||
: [];
|
||||
|
||||
// Add initial serial number input
|
||||
const initialInput = document.createElement('div');
|
||||
initialInput.className = 'serial-number-input';
|
||||
initialInput.innerHTML = `
|
||||
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number">
|
||||
<button type="button" class="btn btn-sm btn-secondary add-serial-number">
|
||||
<i class="fas fa-plus"></i> Add Another
|
||||
</button>
|
||||
`;
|
||||
editSerialNumbersContainer.appendChild(initialInput);
|
||||
|
||||
// Add existing serial numbers
|
||||
if (validSerialNumbers.length > 0) {
|
||||
validSerialNumbers.forEach((serialNumber, index) => {
|
||||
if (index === 0) {
|
||||
// Use the first input we already created
|
||||
editSerialNumbersContainer.querySelector('input').value = serialNumber;
|
||||
} else {
|
||||
const newInput = document.createElement('div');
|
||||
newInput.className = 'serial-number-input';
|
||||
newInput.innerHTML = `
|
||||
<input type="text" name="serial_numbers[]" class="form-control" placeholder="Enter serial number" value="${serialNumber}">
|
||||
<button type="button" class="btn btn-sm btn-danger remove-serial-number">
|
||||
<i class="fas fa-minus"></i> Remove
|
||||
</button>
|
||||
`;
|
||||
editSerialNumbersContainer.appendChild(newInput);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listeners for the serial number buttons
|
||||
editSerialNumbersContainer.querySelectorAll('.add-serial-number').forEach(btn => {
|
||||
btn.addEventListener('click', () => addSerialNumberInput(editSerialNumbersContainer));
|
||||
});
|
||||
|
||||
editSerialNumbersContainer.querySelectorAll('.remove-serial-number').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
this.parentElement.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Show current invoice if exists
|
||||
const currentInvoiceElement = document.getElementById('currentInvoice');
|
||||
if (warranty.invoice_path) {
|
||||
@@ -329,6 +498,20 @@ async function updateWarranty() {
|
||||
|
||||
const formData = new FormData(editWarrantyForm);
|
||||
|
||||
// Get all serial numbers and add them to formData
|
||||
const serialNumbers = [];
|
||||
document.querySelectorAll('#editSerialNumbersContainer input[name="serial_numbers[]"]').forEach(input => {
|
||||
if (input.value.trim()) {
|
||||
serialNumbers.push(input.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the original array and add clean values
|
||||
formData.delete('serial_numbers[]');
|
||||
if (serialNumbers.length > 0) {
|
||||
serialNumbers.forEach(sn => formData.append('serial_numbers', sn));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/${currentWarrantyId}`, {
|
||||
method: 'PUT',
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
/* ===== GLOBAL STYLES ===== */
|
||||
:root {
|
||||
:root[data-theme="light"] {
|
||||
--bg-color: #f5f5f5;
|
||||
--card-bg: #ffffff;
|
||||
--text-color: #333333;
|
||||
--border-color: #e0e0e0;
|
||||
--modal-backdrop: rgba(0, 0, 0, 0.5);
|
||||
--loading-backdrop: rgba(255, 255, 255, 0.8);
|
||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||
|
||||
--primary-color: #3498db;
|
||||
--primary-dark: #2980b9;
|
||||
--secondary-color: #2ecc71;
|
||||
@@ -20,11 +28,29 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--bg-color: #1a1a1a;
|
||||
--card-bg: #2d2d2d;
|
||||
--text-color: #e0e0e0;
|
||||
--border-color: #404040;
|
||||
--modal-backdrop: rgba(0, 0, 0, 0.7);
|
||||
--loading-backdrop: rgba(0, 0, 0, 0.8);
|
||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||
--primary-color: #4dabf7;
|
||||
--primary-dark: #339af0;
|
||||
--secondary-color: #40c057;
|
||||
--danger-color: #fa5252;
|
||||
--light-gray: #2d2d2d;
|
||||
--medium-gray: #404040;
|
||||
--dark-gray: #909090;
|
||||
--white: #2d2d2d;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: var(--light-gray);
|
||||
background-color: var(--bg-color);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -37,16 +63,21 @@ body {
|
||||
|
||||
/* ===== HEADER ===== */
|
||||
header {
|
||||
background-color: var(--white);
|
||||
background-color: var(--card-bg);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 20px 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
header .container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app-title h1 {
|
||||
@@ -76,7 +107,7 @@ header {
|
||||
|
||||
/* ===== FORM PANEL ===== */
|
||||
.form-panel {
|
||||
background-color: var(--white);
|
||||
background-color: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 25px;
|
||||
@@ -204,7 +235,7 @@ header {
|
||||
|
||||
/* ===== WARRANTIES LIST ===== */
|
||||
.warranties-panel {
|
||||
background-color: var(--white);
|
||||
background-color: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 25px;
|
||||
@@ -268,7 +299,7 @@ header {
|
||||
}
|
||||
|
||||
.warranty-card {
|
||||
background-color: var(--white);
|
||||
background-color: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||
padding: 15px;
|
||||
@@ -391,7 +422,7 @@ header {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
background-color: var(--modal-backdrop);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -407,7 +438,7 @@ header {
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: var(--white);
|
||||
background-color: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
width: 90%;
|
||||
@@ -555,18 +586,13 @@ header {
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
background-color: var(--loading-backdrop);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -622,6 +648,147 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== SETTINGS MENU ===== */
|
||||
.settings-container {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--primary-color);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.settings-btn:hover {
|
||||
transform: rotate(30deg);
|
||||
background-color: var(--primary-color);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.settings-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
background-color: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 15px;
|
||||
min-width: 220px;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.settings-menu.active {
|
||||
display: block;
|
||||
animation: slide-in-top 0.3s forwards;
|
||||
}
|
||||
|
||||
@keyframes slide-in-top {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.settings-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-item span {
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--medium-gray);
|
||||
transition: var(--transition);
|
||||
border-radius: 34px;
|
||||
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: var(--white);
|
||||
transition: var(--transition);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.toggle-slider:after {
|
||||
content: '☀️';
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--white);
|
||||
opacity: 0.7;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
input:checked + .toggle-slider:after {
|
||||
content: '🌙';
|
||||
left: auto;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
/* ===== UTILITIES ===== */
|
||||
.text-danger {
|
||||
color: var(--danger-color);
|
||||
|
||||
Reference in New Issue
Block a user