Resolve merge conflicts between feature/css-optimization and main
- Resolved conflicts in admin.html to keep CSS optimization changes - Regenerated package-lock.json after merge - All features from both branches are now integrated
This commit is contained in:
commit
9628da957b
18 changed files with 5638 additions and 386 deletions
|
@ -91,20 +91,40 @@
|
|||
</div>
|
||||
|
||||
<!-- Profanity Filter Tab -->
|
||||
<div id="profanity-tab" class="tab-content profanity-management">
|
||||
<div class="management-section">
|
||||
<h4>🔒 Custom Profanity Words</h4>
|
||||
<form id="add-profanity-form" class="profanity-form">
|
||||
<input type="text" id="new-word" placeholder="Enter word to filter" required>
|
||||
<select id="new-severity">
|
||||
<option value="low">Low</option>
|
||||
<option value="medium" selected>Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
<input type="text" id="new-category" placeholder="Category" value="custom">
|
||||
<button type="submit">Add Word</button>
|
||||
</form>
|
||||
<div id="profanity-tab" class="tab-content">
|
||||
<div class="form-section">
|
||||
<h3>Custom Profanity Words</h3>
|
||||
|
||||
<!-- Add new word form -->
|
||||
<div class="profanity-add-form">
|
||||
<h4>Add Custom Word</h4>
|
||||
<form id="add-profanity-form">
|
||||
<div class="form-row">
|
||||
<input type="text" id="new-word" placeholder="Enter word or phrase" required>
|
||||
<select id="new-severity">
|
||||
<option value="low">Low Severity</option>
|
||||
<option value="medium" selected>Medium Severity</option>
|
||||
<option value="high">High Severity</option>
|
||||
</select>
|
||||
<input type="text" id="new-category" placeholder="Category (optional)" value="custom">
|
||||
<button type="submit">Add Word</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Test profanity filter -->
|
||||
<div class="profanity-test-form">
|
||||
<h4>Test Profanity Filter</h4>
|
||||
<form id="test-profanity-form">
|
||||
<div class="form-row">
|
||||
<input type="text" id="test-text" placeholder="Enter text to test" required>
|
||||
<button type="submit">Test Filter</button>
|
||||
</div>
|
||||
<div id="test-results" class="test-results"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Custom words table -->
|
||||
<table class="locations-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -123,15 +143,6 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="management-section test-section">
|
||||
<h4>🧪 Test Profanity Filter</h4>
|
||||
<form id="test-profanity-form" class="test-form">
|
||||
<textarea id="test-text" placeholder="Enter text to test for profanity..."></textarea>
|
||||
<button type="submit">Test Text</button>
|
||||
</form>
|
||||
<div id="test-results" class="test-results empty">Enter text above to test profanity detection</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -144,6 +155,7 @@
|
|||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://iceymi.b-cdn.net/admin.js"></script>
|
||||
<script src="utils.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
207
public/admin.js
207
public/admin.js
|
@ -71,6 +71,53 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
loadLocations();
|
||||
});
|
||||
|
||||
// Tab navigation logic
|
||||
const tabs = document.querySelectorAll('.tab-btn');
|
||||
const contents = document.querySelectorAll('.tab-content');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', function () {
|
||||
tabs.forEach(t => t.classList.remove('active'));
|
||||
contents.forEach(c => c.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
document.querySelector(`#${this.dataset.tab}-tab`).classList.add('active');
|
||||
|
||||
// Load data for the active tab
|
||||
if (this.dataset.tab === 'profanity') {
|
||||
loadProfanityWords();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Profanity management handlers
|
||||
const addProfanityForm = document.getElementById('add-profanity-form');
|
||||
const testProfanityForm = document.getElementById('test-profanity-form');
|
||||
const profanityTableBody = document.getElementById('profanity-tbody');
|
||||
|
||||
if (addProfanityForm) {
|
||||
addProfanityForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const word = document.getElementById('new-word').value.trim();
|
||||
const severity = document.getElementById('new-severity').value;
|
||||
const category = document.getElementById('new-category').value.trim() || 'custom';
|
||||
|
||||
if (!word) return;
|
||||
|
||||
await addProfanityWord(word, severity, category);
|
||||
});
|
||||
}
|
||||
|
||||
if (testProfanityForm) {
|
||||
testProfanityForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const text = document.getElementById('test-text').value.trim();
|
||||
|
||||
if (!text) return;
|
||||
|
||||
await testProfanityFilter(text);
|
||||
});
|
||||
}
|
||||
|
||||
function showLoginSection() {
|
||||
loginSection.style.display = 'block';
|
||||
adminSection.style.display = 'none';
|
||||
|
@ -445,21 +492,159 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
};
|
||||
|
||||
function getTimeAgo(timestamp) {
|
||||
const now = new Date();
|
||||
const reportTime = new Date(timestamp);
|
||||
const diffInMinutes = Math.floor((now - reportTime) / (1000 * 60));
|
||||
// getTimeAgo and parseUTCDate functions are now available from utils.js
|
||||
|
||||
// Profanity management functions
|
||||
async function loadProfanityWords() {
|
||||
if (!profanityTableBody) return;
|
||||
|
||||
if (diffInMinutes < 1) return 'just now';
|
||||
if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`;
|
||||
try {
|
||||
const response = await fetch('/api/admin/profanity-words', {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
logout('Session expired. Please log in again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
displayProfanityWords(data || []);
|
||||
} else {
|
||||
console.error('Failed to load profanity words:', data.error);
|
||||
profanityTableBody.innerHTML = '<tr><td colspan="6">Failed to load words</td></tr>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading profanity words:', error);
|
||||
profanityTableBody.innerHTML = '<tr><td colspan="6">Error loading words</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayProfanityWords(words) {
|
||||
if (!profanityTableBody) return;
|
||||
|
||||
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||
if (diffInHours < 24) return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`;
|
||||
if (words.length === 0) {
|
||||
profanityTableBody.innerHTML = '<tr><td colspan="6">No custom words added yet</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const diffInDays = Math.floor(diffInHours / 24);
|
||||
if (diffInDays < 7) return `${diffInDays} day${diffInDays !== 1 ? 's' : ''} ago`;
|
||||
profanityTableBody.innerHTML = words.map(word => `
|
||||
<tr>
|
||||
<td>${word.id}</td>
|
||||
<td><code>${escapeHtml(word.word)}</code></td>
|
||||
<td><span class="severity-${word.severity}">${word.severity}</span></td>
|
||||
<td>${word.category || 'N/A'}</td>
|
||||
<td>${new Date(word.created_at).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<button class="action-btn danger" onclick="deleteProfanityWord(${word.id})">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function addProfanityWord(word, severity, category) {
|
||||
try {
|
||||
const response = await fetch('/api/admin/profanity-words', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
},
|
||||
body: JSON.stringify({ word, severity, category })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Clear form
|
||||
document.getElementById('new-word').value = '';
|
||||
document.getElementById('new-category').value = 'custom';
|
||||
document.getElementById('new-severity').value = 'medium';
|
||||
|
||||
// Reload words list
|
||||
loadProfanityWords();
|
||||
|
||||
console.log('Word added successfully');
|
||||
} else {
|
||||
alert('Failed to add word: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding profanity word:', error);
|
||||
alert('Error adding word. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async function testProfanityFilter(text) {
|
||||
const resultsDiv = document.getElementById('test-results');
|
||||
if (!resultsDiv) return;
|
||||
|
||||
return reportTime.toLocaleDateString();
|
||||
try {
|
||||
const response = await fetch('/api/admin/test-profanity', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
},
|
||||
body: JSON.stringify({ text })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (data.analysis && data.analysis.hasProfanity) {
|
||||
resultsDiv.className = 'test-results profane';
|
||||
resultsDiv.innerHTML = `
|
||||
<strong>⚠️ Profanity Detected!</strong><br>
|
||||
Detected words: ${data.analysis.matches.map(m => `<code>${escapeHtml(m.word)}</code>`).join(', ')}<br>
|
||||
Severity: ${data.analysis.severity}<br>
|
||||
Filtered text: "${escapeHtml(data.filtered)}"
|
||||
`;
|
||||
} else {
|
||||
resultsDiv.className = 'test-results clean';
|
||||
resultsDiv.innerHTML = '<strong>✅ Text is clean!</strong><br>No profanity detected.';
|
||||
}
|
||||
} else {
|
||||
resultsDiv.className = 'test-results empty';
|
||||
resultsDiv.innerHTML = 'Error testing text: ' + data.error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error testing profanity filter:', error);
|
||||
resultsDiv.className = 'test-results empty';
|
||||
resultsDiv.innerHTML = 'Error testing text. Please try again.';
|
||||
}
|
||||
}
|
||||
|
||||
// Make deleteProfanityWord available globally for onclick handlers
|
||||
window.deleteProfanityWord = async function(wordId) {
|
||||
if (!confirm('Are you sure you want to delete this word?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/profanity-words/${wordId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadProfanityWords();
|
||||
console.log('Word deleted successfully');
|
||||
} else {
|
||||
const data = await response.json();
|
||||
alert('Failed to delete word: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting profanity word:', error);
|
||||
alert('Error deleting word. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Initialize theme toggle
|
||||
|
|
|
@ -82,19 +82,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
});
|
||||
};
|
||||
|
||||
const getTimeAgo = (timestamp) => {
|
||||
const now = new Date();
|
||||
const reportTime = new Date(timestamp);
|
||||
const diffInMinutes = Math.floor((now - reportTime) / (1000 * 60));
|
||||
|
||||
if (diffInMinutes < 1) return 'just now';
|
||||
if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||
if (diffInHours < 24) return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`;
|
||||
|
||||
return 'over a day ago';
|
||||
};
|
||||
// getTimeAgo function is now available from utils.js
|
||||
|
||||
const refreshLocations = () => {
|
||||
fetch('/api/locations')
|
||||
|
|
|
@ -93,19 +93,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
});
|
||||
};
|
||||
|
||||
const getTimeAgo = (timestamp) => {
|
||||
const now = new Date();
|
||||
const reportTime = new Date(timestamp);
|
||||
const diffInMinutes = Math.floor((now - reportTime) / (1000 * 60));
|
||||
|
||||
if (diffInMinutes < 1) return 'just now';
|
||||
if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||
if (diffInHours < 24) return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`;
|
||||
|
||||
return 'over a day ago';
|
||||
};
|
||||
// getTimeAgo function is now available from utils.js
|
||||
|
||||
// Toggle between map and table view
|
||||
const switchView = (viewType) => {
|
||||
|
@ -165,45 +153,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
tableLocationCount.textContent = `${locations.length} active report${locations.length !== 1 ? 's' : ''}`;
|
||||
};
|
||||
|
||||
// Calculate time remaining until 24-hour expiration
|
||||
const getTimeRemaining = (timestamp, persistent = false) => {
|
||||
if (persistent) {
|
||||
return 'Persistent';
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const reportTime = new Date(timestamp);
|
||||
const expirationTime = new Date(reportTime.getTime() + 24 * 60 * 60 * 1000);
|
||||
const remaining = expirationTime - now;
|
||||
|
||||
if (remaining <= 0) return 'Expired';
|
||||
|
||||
const hours = Math.floor(remaining / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
};
|
||||
|
||||
// Get CSS class for time remaining
|
||||
const getRemainingClass = (timestamp, persistent = false) => {
|
||||
if (persistent) {
|
||||
return 'normal'; // Use normal styling for persistent reports
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const reportTime = new Date(timestamp);
|
||||
const expirationTime = new Date(reportTime.getTime() + 24 * 60 * 60 * 1000);
|
||||
const remaining = expirationTime - now;
|
||||
const hoursRemaining = remaining / (1000 * 60 * 60);
|
||||
|
||||
if (hoursRemaining <= 1) return 'urgent';
|
||||
if (hoursRemaining <= 6) return 'warning';
|
||||
return 'normal';
|
||||
};
|
||||
// getTimeRemaining and getRemainingClass functions are now available from utils.js
|
||||
|
||||
const refreshLocations = () => {
|
||||
fetch('/api/locations')
|
||||
|
@ -453,7 +403,14 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
}),
|
||||
});
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
return res.json().then(errorData => {
|
||||
throw { status: res.status, data: errorData };
|
||||
});
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(location => {
|
||||
refreshLocations();
|
||||
messageDiv.textContent = 'Location reported successfully!';
|
||||
|
@ -462,17 +419,34 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
})
|
||||
.catch(err => {
|
||||
console.error('Error reporting location:', err);
|
||||
messageDiv.textContent = 'Error reporting location.';
|
||||
messageDiv.className = 'message error';
|
||||
|
||||
// Handle specific profanity rejection
|
||||
if (err.status === 400 && err.data && err.data.error === 'Submission rejected') {
|
||||
messageDiv.textContent = err.data.message;
|
||||
messageDiv.className = 'message error profanity-rejection';
|
||||
|
||||
// Clear the description field to encourage rewriting
|
||||
document.getElementById('description').value = '';
|
||||
document.getElementById('description').focus();
|
||||
} else if (err.data && err.data.error) {
|
||||
messageDiv.textContent = err.data.error;
|
||||
messageDiv.className = 'message error';
|
||||
} else {
|
||||
messageDiv.textContent = 'Error reporting location. Please try again.';
|
||||
messageDiv.className = 'message error';
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
submitBtn.disabled = false;
|
||||
submitText.style.display = 'inline';
|
||||
submitLoading.style.display = 'none';
|
||||
messageDiv.style.display = 'block';
|
||||
|
||||
// Longer timeout for profanity rejection messages
|
||||
const timeout = messageDiv.className.includes('profanity-rejection') ? 15000 : 3000;
|
||||
setTimeout(() => {
|
||||
messageDiv.style.display = 'none';
|
||||
}, 3000);
|
||||
}, timeout);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -74,19 +74,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const getTimeAgo = (timestamp) => {
|
||||
const now = new Date();
|
||||
const reportTime = new Date(timestamp);
|
||||
const diffInMinutes = Math.floor((now - reportTime) / (1000 * 60));
|
||||
|
||||
if (diffInMinutes < 1) return 'just now';
|
||||
if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||
if (diffInHours < 24) return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`;
|
||||
|
||||
return 'over a day ago';
|
||||
};
|
||||
// getTimeAgo function is now available from utils.js
|
||||
|
||||
const refreshLocations = () => {
|
||||
fetch('/api/locations')
|
||||
|
|
|
@ -41,7 +41,8 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
|
|||
<div class="form-group">
|
||||
<label for="description">Additional Details (Optional)</label>
|
||||
<textarea id="description" name="description" rows="3"
|
||||
placeholder="Number of vehicles, time observed, etc."></textarea>
|
||||
placeholder="Number of vehicles, time observed, etc." aria-describedby="description-help"></textarea>
|
||||
<small id="description-help" class="input-help">Keep descriptions appropriate and relevant to road conditions. Submissions with inappropriate language will be rejected.</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submit-btn">
|
||||
|
@ -108,6 +109,7 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
|
|||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="utils.js"></script>
|
||||
<script src="app-mapbox.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
96
public/utils.js
Normal file
96
public/utils.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* Shared utility functions for the Great Lakes Ice Report frontend
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper function to parse UTC date consistently across all frontend scripts
|
||||
* Ensures timestamp is treated as UTC if it doesn't have timezone info
|
||||
* @param {string} timestamp - The timestamp string to parse
|
||||
* @returns {Date} - Parsed Date object with proper UTC interpretation
|
||||
*/
|
||||
const parseUTCDate = (timestamp) => {
|
||||
return new Date(timestamp.includes('T') ? timestamp : timestamp + 'Z');
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate human-readable time ago string
|
||||
* @param {string} timestamp - The timestamp to calculate from
|
||||
* @returns {string} - Human-readable time difference
|
||||
*/
|
||||
const getTimeAgo = (timestamp) => {
|
||||
const now = new Date();
|
||||
const reportTime = parseUTCDate(timestamp);
|
||||
const diffInMinutes = Math.floor((now - reportTime) / (1000 * 60));
|
||||
|
||||
if (diffInMinutes < 1) return 'just now';
|
||||
if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||
if (diffInHours < 24) return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`;
|
||||
|
||||
const diffInDays = Math.floor(diffInHours / 24);
|
||||
if (diffInDays < 7) return `${diffInDays} day${diffInDays !== 1 ? 's' : ''} ago`;
|
||||
|
||||
return reportTime.toLocaleDateString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate time remaining until 48-hour expiration
|
||||
* @param {string} timestamp - The creation timestamp
|
||||
* @param {boolean} persistent - Whether the report is persistent
|
||||
* @returns {string} - Time remaining string
|
||||
*/
|
||||
const getTimeRemaining = (timestamp, persistent = false) => {
|
||||
if (persistent) {
|
||||
return 'Persistent';
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const reportTime = parseUTCDate(timestamp);
|
||||
const expirationTime = new Date(reportTime.getTime() + 48 * 60 * 60 * 1000);
|
||||
const remaining = expirationTime - now;
|
||||
|
||||
if (remaining <= 0) return 'Expired';
|
||||
|
||||
const hours = Math.floor(remaining / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get CSS class for time remaining styling
|
||||
* @param {string} timestamp - The creation timestamp
|
||||
* @param {boolean} persistent - Whether the report is persistent
|
||||
* @returns {string} - CSS class name
|
||||
*/
|
||||
const getRemainingClass = (timestamp, persistent = false) => {
|
||||
if (persistent) {
|
||||
return 'normal'; // Use normal styling for persistent reports
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const reportTime = parseUTCDate(timestamp);
|
||||
const expirationTime = new Date(reportTime.getTime() + 48 * 60 * 60 * 1000);
|
||||
const remaining = expirationTime - now;
|
||||
const hoursRemaining = remaining / (1000 * 60 * 60);
|
||||
|
||||
if (hoursRemaining <= 1) return 'urgent';
|
||||
if (hoursRemaining <= 6) return 'warning';
|
||||
return 'normal';
|
||||
};
|
||||
|
||||
// Export functions for module usage (if using ES6 modules in the future)
|
||||
// For now, functions are available globally when script is included
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
parseUTCDate,
|
||||
getTimeAgo,
|
||||
getTimeRemaining,
|
||||
getRemainingClass
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue