Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ RUN npm ci --omit=dev && npm cache clean --force
# Copy the rest of the application code
COPY . .

# Create uploads directory
RUN mkdir -p public/uploads
# Create uploads directory structure
RUN mkdir -p public/uploads/images-driver public/uploads/images-circuit

# Expose the port the app runs on
EXPOSE 3001
Expand Down
5 changes: 2 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ services:
- LOG_LEVEL=${LOG_LEVEL:-info}
- NODE_ENV=${NODE_ENV:-production}
volumes:
- uploads_shared:/usr/src/app/public/uploads
- ./public/uploads:/usr/src/app/public/uploads
- ./logs:/usr/src/app/logs
depends_on:
db:
Expand Down Expand Up @@ -79,7 +79,7 @@ services:
- ./config/Caddyfile.http:/etc/caddy/Caddyfile:ro
# For HTTPS production mode (Let's Encrypt):
# - ./config/Caddyfile.https-production:/etc/caddy/Caddyfile:ro
- uploads_shared:/var/www/html/uploads:ro
- ./public/uploads:/var/www/html/uploads:ro
- caddy_data:/data
- caddy_config:/config
- caddy_logs:/var/log/caddy
Expand All @@ -102,7 +102,6 @@ volumes:
caddy_data:
caddy_config:
caddy_logs:
uploads_shared:

networks:
app-network:
Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
"connect-pg-simple": "^10.0.0",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"express-session": "^1.18.2",
Expand Down
18 changes: 15 additions & 3 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ <h3>ADD NEW DRIVER</h3>
</div>
<div class="form-group">
<label for="profilePictureAdd">Profile Picture (Optional)</label>
<div class="picture-preview-container" id="addPicturePreviewContainer" style="display: none;">
<img id="addPicturePreview" src="" alt="Profile picture preview" style="max-width: 150px; max-height: 150px; border-radius: 8px; margin-bottom: 10px;">
<p class="preview-label">Preview - New Profile Picture</p>
</div>
<input type="file" id="profilePictureAdd" name="profilePicture" accept="image/*">
<small class="form-help">Max file size: 5MB. Supported formats: JPG, PNG, GIF, WebP</small>
</div>
Expand Down Expand Up @@ -142,15 +146,19 @@ <h3>EDIT DRIVER</h3>
<img id="currentPicture" src="" alt="Current profile picture" style="display: none;">
<p id="noPictureText">No profile picture uploaded</p>
</div>
<div class="picture-preview-container" id="editPicturePreviewContainer" style="display: none;">
<img id="editPicturePreview" src="" alt="New profile picture preview" style="max-width: 150px; max-height: 150px; border-radius: 8px; margin-bottom: 10px;">
<p class="preview-label">Preview - New Profile Picture</p>
</div>
<div class="picture-actions">
<button type="button" class="btn btn-small btn-secondary" data-action="triggerFileUpload">
<button type="button" class="btn btn-small btn-secondary" data-action="triggerEditFileUpload">
<i class="fas fa-upload"></i> UPLOAD NEW
</button>
<button type="button" class="btn btn-small btn-delete" data-action="deleteProfilePicture" id="deletePictureBtn" style="display: none;">
<i class="fas fa-trash"></i> DELETE
</button>
</div>
<input type="file" id="profilePictureEdit" accept="image/*" style="display: none;" onchange="uploadProfilePicture()">
<input type="file" id="profilePictureEdit" name="profilePicture" accept="image/*" style="display: none;">
<small class="form-help">Max file size: 5MB. Supported formats: JPG, PNG, GIF, WebP</small>
</div>
<div class="form-actions">
Expand Down Expand Up @@ -192,6 +200,10 @@ <h3>RACE SETTINGS</h3>
<img id="currentCircuitImage" src="" alt="Current circuit image" style="display: none;">
<p id="noCircuitImageText">No circuit image uploaded</p>
</div>
<div class="picture-preview-container" id="circuitImagePreviewContainer" style="display: none;">
<img id="circuitImagePreview" src="" alt="New circuit image preview" style="max-width: 200px; max-height: 150px; border-radius: 8px; margin-bottom: 10px;">
<p class="preview-label">Preview - New Circuit Image</p>
</div>
<div class="picture-actions">
<button type="button" class="btn btn-small btn-secondary" data-action="triggerCircuitImageUpload">
<i class="fas fa-upload"></i> UPLOAD IMAGE
Expand All @@ -200,7 +212,7 @@ <h3>RACE SETTINGS</h3>
<i class="fas fa-trash"></i> DELETE
</button>
</div>
<input type="file" id="circuitImageUpload" accept="image/*" style="display: none;" onchange="uploadCircuitImage()">
<input type="file" id="circuitImageUpload" accept="image/*" style="display: none;">
<small class="form-help">Max file size: 5MB. Supported formats: JPG, PNG, GIF, WebP</small>
</div>
</div>
Expand Down
44 changes: 43 additions & 1 deletion public/js/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createDriver,
uploadProfilePicture,
} from './api.js'
import { clearPreview } from './imagePreview.js'
import { loadLeaderboard } from './leaderboard.js'
import { closeAddDriverModal, closeEditDriverModal } from './modals.js'
import { showNotification } from './notifications.js'
Expand Down Expand Up @@ -76,6 +77,13 @@ export async function handleEditDriver(event) {

try {
await apiUpdateDriver(driverId, driverData)

// If there's a new profile picture, upload it
const profilePictureFile = formData.get('profilePicture')
if (profilePictureFile && profilePictureFile.size > 0) {
await uploadProfilePictureForDriver(driverId, profilePictureFile)
}

closeEditDriverModal()
await loadLeaderboard()
showNotification('Driver updated successfully!', 'success')
Expand Down Expand Up @@ -122,13 +130,47 @@ export async function deleteDriver(driverId) {
// Upload profile picture for a driver
async function uploadProfilePictureForDriver(driverId, file) {
try {
await uploadProfilePicture(driverId, file)
const result = await uploadProfilePicture(driverId, file)

// If we're in the edit modal, update the current picture display immediately
const editDriverId = document.getElementById('editDriverId')
if (editDriverId && editDriverId.value === driverId.toString()) {
updateEditModalProfilePicture(result.profilePicture)
// Clear the preview since we now show the actual uploaded image
clearPreview('editPicturePreviewContainer', 'editPicturePreview')
}

// Also clear preview for add modal if applicable
const addModal = document.getElementById('addDriverModal')
if (addModal && addModal.style.display === 'block') {
clearPreview('addPicturePreviewContainer', 'addPicturePreview')
}
} catch (_error) {
showNotification('Failed to upload profile picture', 'warning')
// Don't throw - we don't want to prevent driver creation if picture upload fails
}
}

// Update the profile picture display in the edit modal
function updateEditModalProfilePicture(profilePictureUrl) {
const currentPicture = document.getElementById('currentPicture')
const noPictureText = document.getElementById('noPictureText')
const deletePictureBtn = document.getElementById('deletePictureBtn')

if (currentPicture && profilePictureUrl) {
currentPicture.src = profilePictureUrl
currentPicture.style.display = 'block'

if (noPictureText) {
noPictureText.style.display = 'none'
}

if (deletePictureBtn) {
deletePictureBtn.style.display = 'inline-flex'
}
}
}

// Delete profile picture
export async function deleteProfilePicture() {
const driverId = document.getElementById('editDriverId').value
Expand Down
114 changes: 114 additions & 0 deletions public/js/imagePreview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Image preview functionality for profile pictures

import { uploadCircuitImage } from './api.js'
import { showNotification } from './notifications.js'
import { loadRaceSettings, populateSettingsModal } from './settings.js'

/**
* Setup image preview for file input
* @param {string} fileInputId - ID of the file input element
* @param {string} previewContainerId - ID of the preview container
* @param {string} previewImageId - ID of the preview image element
*/
export function setupImagePreview(fileInputId, previewContainerId, previewImageId) {
const fileInput = document.getElementById(fileInputId)
const previewContainer = document.getElementById(previewContainerId)
const previewImage = document.getElementById(previewImageId)

if (!fileInput || !previewContainer || !previewImage) {
return
}

fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0]

if (file) {
// Validate file type
if (!file.type.startsWith('image/')) {
showNotification('Please select a valid image file', 'error')
clearPreview(previewContainerId, previewImageId)
return
}

// Validate file size (5MB limit)
const maxSize = 5 * 1024 * 1024 // 5MB in bytes
if (file.size > maxSize) {
showNotification('File size must be less than 5MB', 'error')
clearPreview(previewContainerId, previewImageId)
fileInput.value = '' // Clear the input
return
}

// Create and display preview
const reader = new FileReader()
reader.onload = (e) => {
previewImage.src = e.target.result
previewContainer.style.display = 'block'
}
reader.readAsDataURL(file)

// For circuit images, upload immediately
if (fileInputId === 'circuitImageUpload') {
try {
await uploadCircuitImage(file)
showNotification('Circuit image uploaded successfully!', 'success')

// Refresh the race settings and modal
await loadRaceSettings()
populateSettingsModal()

// Clear the preview since we now show the actual uploaded image
clearPreview(previewContainerId, previewImageId)
fileInput.value = '' // Clear the file input
} catch (_error) {
showNotification('Failed to upload circuit image. Please try again.', 'error')
clearPreview(previewContainerId, previewImageId)
fileInput.value = '' // Clear the file input
}
}
} else {
clearPreview(previewContainerId, previewImageId)
}
})
}

/**
* Clear image preview
* @param {string} previewContainerId - ID of the preview container
* @param {string} previewImageId - ID of the preview image element
*/
export function clearPreview(previewContainerId, previewImageId) {
const previewContainer = document.getElementById(previewContainerId)
const previewImage = document.getElementById(previewImageId)

if (previewContainer) {
previewContainer.style.display = 'none'
}

if (previewImage) {
previewImage.src = ''
}
}

/**
* Clear all previews (useful when modals are closed)
*/
export function clearAllPreviews() {
clearPreview('addPicturePreviewContainer', 'addPicturePreview')
clearPreview('editPicturePreviewContainer', 'editPicturePreview')
clearPreview('circuitImagePreviewContainer', 'circuitImagePreview')
}

/**
* Setup all image previews for the application
*/
export function initializeImagePreviews() {
// Setup preview for add driver modal
setupImagePreview('profilePictureAdd', 'addPicturePreviewContainer', 'addPicturePreview')
Copy link

Copilot AI Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file input ID 'profilePictureAdd' doesn't match the actual ID 'profilePictureAdd' in the HTML, but the HTML shows 'profilePictureAdd' while other references in the codebase use 'profilePictureInput'. This mismatch could prevent the preview functionality from working for the add driver modal.

Suggested change
setupImagePreview('profilePictureAdd', 'addPicturePreviewContainer', 'addPicturePreview')
setupImagePreview('profilePictureInput', 'addPicturePreviewContainer', 'addPicturePreview')

Copilot uses AI. Check for mistakes.

// Setup preview for edit driver modal
setupImagePreview('profilePictureEdit', 'editPicturePreviewContainer', 'editPicturePreview')

// Setup preview for circuit image upload
setupImagePreview('circuitImageUpload', 'circuitImagePreviewContainer', 'circuitImagePreview')
}
6 changes: 6 additions & 0 deletions public/js/main.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { checkAuthStatus, logout } from './auth.js'
import { deleteDriver, deleteProfilePicture, setupFormEventListeners } from './forms.js'
import { initializeImagePreviews } from './imagePreview.js'
import { loadLeaderboard } from './leaderboard.js'
import {
closeAddDriverModal,
Expand All @@ -10,6 +11,7 @@ import {
showEditDriverModal,
showSettingsModal,
triggerCircuitImageUpload,
triggerEditFileUpload,
triggerFileUpload,
} from './modals.js'
import {
Expand All @@ -26,6 +28,7 @@ function initializeApp() {
loadRaceSettings()
loadLeaderboard()
setupEventListeners()
initializeImagePreviews()
}

// Setup all event listeners
Expand Down Expand Up @@ -131,6 +134,9 @@ function handleAction(action) {
case 'triggerFileUpload':
triggerFileUpload()
break
case 'triggerEditFileUpload':
triggerEditFileUpload()
break
case 'deleteProfilePicture':
deleteProfilePicture()
break
Expand Down
10 changes: 9 additions & 1 deletion public/js/modals.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { clearPreview } from './imagePreview.js'
import { showNotification } from './notifications.js'
import { findDriverById, getIsAuthorized } from './state.js'

Expand All @@ -16,6 +17,7 @@ export function showAddDriverModal() {
export function closeAddDriverModal() {
document.getElementById('addDriverModal').style.display = 'none'
document.getElementById('addDriverForm').reset()
clearPreview('addPicturePreviewContainer', 'addPicturePreview')
}

// Show edit driver modal
Expand Down Expand Up @@ -56,6 +58,7 @@ export function showEditDriverModal(driverId) {
export function closeEditDriverModal() {
document.getElementById('editDriverModal').style.display = 'none'
document.getElementById('editDriverForm').reset()
clearPreview('editPicturePreviewContainer', 'editPicturePreview')
}

// Show settings modal
Expand Down Expand Up @@ -83,6 +86,7 @@ export function showSettingsModal() {
export function closeSettingsModal() {
document.getElementById('settingsModal').style.display = 'none'
document.getElementById('settingsForm').reset()
clearPreview('circuitImagePreviewContainer', 'circuitImagePreview')
}

// Setup modal event listeners
Expand Down Expand Up @@ -110,6 +114,10 @@ export function triggerFileUpload() {
document.getElementById('profilePictureInput').click()
}

export function triggerEditFileUpload() {
document.getElementById('profilePictureEdit').click()
}

export function triggerCircuitImageUpload() {
document.getElementById('circuitImageInput').click()
document.getElementById('circuitImageUpload').click()
Copy link

Copilot AI Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ID 'circuitImageUpload' doesn't match the ID referenced in triggerFileUpload() function which uses 'profilePictureInput'. This inconsistency could cause the function to fail silently if the element doesn't exist.

Suggested change
document.getElementById('circuitImageUpload').click()
document.getElementById('profilePictureInput').click()

Copilot uses AI. Check for mistakes.
}
Loading
Loading