ajout de déplacement des carte affichées dans le dashboard et les tryouts
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+115
-1
@@ -325,12 +325,30 @@ a:hover { color: var(--primary-dark); }
|
|||||||
/* Dashboard Grid */
|
/* Dashboard Grid */
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Allow cards to span full width or be placed flexibly */
|
||||||
|
.dashboard-grid .card {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card position control - can be set via data-position attribute */
|
||||||
|
.dashboard-grid .card[data-position="right"] {
|
||||||
|
grid-column: 2 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-grid .card[data-position="left"] {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.dashboard-grid { grid-template-columns: 1fr; }
|
.dashboard-grid { grid-template-columns: 1fr; }
|
||||||
|
.dashboard-grid .card[data-position="right"],
|
||||||
|
.dashboard-grid .card[data-position="left"] {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tables */
|
/* Tables */
|
||||||
@@ -1499,3 +1517,99 @@ a:hover { color: var(--primary-dark); }
|
|||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Draggable Blocks */
|
||||||
|
.drag-handle {
|
||||||
|
cursor: move;
|
||||||
|
padding: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--gray-400);
|
||||||
|
transition: var(--transition);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle:hover {
|
||||||
|
background: var(--gray-100);
|
||||||
|
color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(0.98);
|
||||||
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drop placeholder - shows where card will be inserted */
|
||||||
|
.sortable-placeholder {
|
||||||
|
background: var(--primary-light);
|
||||||
|
border: 2px dashed var(--primary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
min-height: 100px;
|
||||||
|
opacity: 0.5;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode placeholder */
|
||||||
|
[data-theme="dark"] .sortable-placeholder {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ghost is now controlled via inline styles in JS */
|
||||||
|
.sortable-ghost {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-chosen {
|
||||||
|
transform: scale(1.01);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header .drag-handle {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header .drag-handle:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode drag handle */
|
||||||
|
[data-theme="dark"] .drag-handle {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .drag-handle:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make cards draggable by header */
|
||||||
|
.card-header {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header.draggable {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header.draggable:hover .drag-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header.draggable .drag-handle {
|
||||||
|
opacity: 0.4;
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|||||||
+290
-10
@@ -45,11 +45,300 @@ document.addEventListener('click', function(event) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-dismiss flash messages
|
// Confirm delete actions
|
||||||
|
document.querySelectorAll('[data-confirm]').forEach(function(el) {
|
||||||
|
el.addEventListener('click', function(e) {
|
||||||
|
if (!confirm(this.getAttribute('data-confirm'))) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draggable Blocks Functionality
|
||||||
|
function initDraggableBlocks() {
|
||||||
|
const grids = document.querySelectorAll('.dashboard-grid');
|
||||||
|
|
||||||
|
grids.forEach(function(grid) {
|
||||||
|
const cards = grid.querySelectorAll('.card');
|
||||||
|
|
||||||
|
// Add drag handles to card headers
|
||||||
|
cards.forEach(function(card, index) {
|
||||||
|
const header = card.querySelector('.card-header');
|
||||||
|
if (header) {
|
||||||
|
// Add unique ID to card if not present
|
||||||
|
if (!card.id) {
|
||||||
|
card.id = 'block-' + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create drag handle
|
||||||
|
const dragHandle = document.createElement('div');
|
||||||
|
dragHandle.className = 'drag-handle';
|
||||||
|
dragHandle.innerHTML = '<i class="fas fa-grip-vertical"></i>';
|
||||||
|
dragHandle.title = 'Drag to reorder';
|
||||||
|
|
||||||
|
// Add draggable class to header
|
||||||
|
header.classList.add('draggable');
|
||||||
|
header.prepend(dragHandle);
|
||||||
|
|
||||||
|
// Make header draggable
|
||||||
|
makeHeaderDraggable(header, card, grid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load saved layout
|
||||||
|
loadLayout(grid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeHeaderDraggable(header, card, grid) {
|
||||||
|
let draggedElement = null;
|
||||||
|
let placeholder = null;
|
||||||
|
let ghost = null;
|
||||||
|
let animationFrame = null;
|
||||||
|
|
||||||
|
header.addEventListener('mousedown', function(e) {
|
||||||
|
// Only start drag from drag handle
|
||||||
|
if (!e.target.closest('.drag-handle')) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
draggedElement = card;
|
||||||
|
|
||||||
|
// Get card dimensions
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const offsetX = e.clientX - rect.left;
|
||||||
|
const offsetY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
// Store original data-position to restore later
|
||||||
|
const originalDataPosition = card.getAttribute('data-position');
|
||||||
|
|
||||||
|
// Remove data-position during drag to allow free placement
|
||||||
|
if (originalDataPosition) {
|
||||||
|
card.removeAttribute('data-position');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ghost element (floating preview)
|
||||||
|
ghost = document.createElement('div');
|
||||||
|
ghost.className = 'sortable-ghost';
|
||||||
|
ghost.style.position = 'fixed';
|
||||||
|
ghost.style.pointerEvents = 'none';
|
||||||
|
ghost.style.zIndex = '99999';
|
||||||
|
ghost.style.width = rect.width + 'px';
|
||||||
|
ghost.style.height = rect.height + 'px';
|
||||||
|
ghost.style.backgroundColor = 'var(--card-bg, white)';
|
||||||
|
ghost.style.border = '2px solid var(--primary)';
|
||||||
|
ghost.style.borderRadius = 'var(--radius)';
|
||||||
|
ghost.style.opacity = '0.9';
|
||||||
|
ghost.style.boxShadow = 'var(--shadow-lg)';
|
||||||
|
ghost.style.cursor = 'grabbing';
|
||||||
|
document.body.appendChild(ghost);
|
||||||
|
|
||||||
|
// Create placeholder element (shows drop position)
|
||||||
|
placeholder = document.createElement('div');
|
||||||
|
placeholder.className = 'sortable-placeholder';
|
||||||
|
placeholder.style.minHeight = rect.height + 'px';
|
||||||
|
card.parentNode.insertBefore(placeholder, card);
|
||||||
|
|
||||||
|
// Keep original card visible but add dragging style
|
||||||
|
card.classList.add('dragging');
|
||||||
|
card.style.opacity = '0.5';
|
||||||
|
card.style.transform = 'scale(0.98)';
|
||||||
|
|
||||||
|
// Initial position
|
||||||
|
ghost.style.top = (e.clientY - offsetY) + 'px';
|
||||||
|
ghost.style.left = (e.clientX - offsetX) + 'px';
|
||||||
|
|
||||||
|
function updateGhostPosition(clientY, clientX) {
|
||||||
|
// Position ghost directly at cursor position
|
||||||
|
ghost.style.top = (clientY - offsetY) + 'px';
|
||||||
|
ghost.style.left = (clientX - offsetX) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get grid layout info for 2D position tracking
|
||||||
|
const gridRect = grid.getBoundingClientRect();
|
||||||
|
const gridStyle = window.getComputedStyle(grid);
|
||||||
|
const gridGap = parseInt(gridStyle.gap) || 20;
|
||||||
|
|
||||||
|
// Calculate column positions for 2D grid
|
||||||
|
function getColumnFromX(x) {
|
||||||
|
// Calculate which column the x position falls into
|
||||||
|
const relativeX = x - gridRect.left;
|
||||||
|
const colWidth = (gridRect.width + gridGap) / Math.max(1, Math.floor(gridRect.width / 300)); // Estimate columns based on min 300px width
|
||||||
|
return Math.floor(relativeX / (colWidth + gridGap));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Cancel any pending animation frame
|
||||||
|
if (animationFrame) {
|
||||||
|
cancelAnimationFrame(animationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use requestAnimationFrame for smooth updates
|
||||||
|
animationFrame = requestAnimationFrame(function() {
|
||||||
|
updateGhostPosition(e.clientY, e.clientX);
|
||||||
|
|
||||||
|
// Use a point slightly offset from cursor to avoid ghost interference
|
||||||
|
// This ensures we detect the card under the cursor, not the ghost
|
||||||
|
const checkX = e.clientX;
|
||||||
|
const checkY = e.clientY + 10; // 10px below cursor for better detection
|
||||||
|
|
||||||
|
// Find the element under the offset point
|
||||||
|
const elementUnderCursor = document.elementFromPoint(checkX, checkY);
|
||||||
|
|
||||||
|
// Check if we're directly over the placeholder (skip processing to avoid flicker)
|
||||||
|
if (elementUnderCursor && elementUnderCursor.closest('.sortable-placeholder')) {
|
||||||
|
return; // Already over placeholder, don't change position
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the card that contains or is the element under cursor
|
||||||
|
let targetCard = null;
|
||||||
|
if (elementUnderCursor) {
|
||||||
|
targetCard = elementUnderCursor.closest('.card');
|
||||||
|
// Make sure it's not the dragged card
|
||||||
|
if (targetCard && targetCard.classList.contains('dragging')) {
|
||||||
|
targetCard = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a valid target card, determine insert position
|
||||||
|
if (targetCard) {
|
||||||
|
const targetRect = targetCard.getBoundingClientRect();
|
||||||
|
const targetMiddleY = targetRect.top + targetRect.height / 2;
|
||||||
|
|
||||||
|
// If cursor is above the card's middle, insert before it
|
||||||
|
// If cursor is below, insert after it
|
||||||
|
// Use checkY for consistent comparison with detection point
|
||||||
|
if (checkY < targetMiddleY) {
|
||||||
|
// Check if placeholder is already in the correct position
|
||||||
|
if (targetCard.nextElementSibling !== placeholder) {
|
||||||
|
grid.insertBefore(placeholder, targetCard);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Insert after the target card
|
||||||
|
const nextCard = targetCard.nextElementSibling;
|
||||||
|
// If next sibling is the placeholder or the dragged element, it's already in the right position
|
||||||
|
if (nextCard === placeholder || nextCard === draggedElement) {
|
||||||
|
// Already in correct position, don't move
|
||||||
|
} else if (nextCard && nextCard.classList.contains('card')) {
|
||||||
|
grid.insertBefore(placeholder, nextCard);
|
||||||
|
} else {
|
||||||
|
grid.appendChild(placeholder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No target card found - check if we should move to end
|
||||||
|
// This handles the case where we're dragging over empty space or the grid background
|
||||||
|
// If cursor is within the grid bounds, move to end
|
||||||
|
if (e.clientX >= gridRect.left && e.clientX <= gridRect.right &&
|
||||||
|
e.clientY >= gridRect.top && e.clientY <= gridRect.bottom) {
|
||||||
|
// Check if we're over the dragged element (which is hidden but still in DOM)
|
||||||
|
const isOverDraggedElement = elementUnderCursor &&
|
||||||
|
(elementUnderCursor.closest('.card.dragging') ||
|
||||||
|
elementUnderCursor.closest('.sortable-placeholder'));
|
||||||
|
|
||||||
|
if (!isOverDraggedElement) {
|
||||||
|
// Move placeholder to end if not already there
|
||||||
|
const lastCard = grid.querySelector('.card:last-of-type');
|
||||||
|
if (lastCard && lastCard.nextElementSibling !== placeholder) {
|
||||||
|
grid.appendChild(placeholder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp(e) {
|
||||||
|
if (animationFrame) {
|
||||||
|
cancelAnimationFrame(animationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draggedElement) {
|
||||||
|
draggedElement.classList.remove('dragging');
|
||||||
|
// Restore original card styles
|
||||||
|
draggedElement.style.opacity = '';
|
||||||
|
draggedElement.style.transform = '';
|
||||||
|
|
||||||
|
// Ensure the card is placed in the correct position
|
||||||
|
// The placeholder shows where the card should go
|
||||||
|
if (placeholder && placeholder.parentNode) {
|
||||||
|
// Move the dragged element to the placeholder position
|
||||||
|
placeholder.parentNode.replaceChild(draggedElement, placeholder);
|
||||||
|
} else {
|
||||||
|
// If no placeholder exists, append to the end
|
||||||
|
grid.appendChild(draggedElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save layout
|
||||||
|
saveLayout(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ghost && ghost.parentNode) {
|
||||||
|
ghost.parentNode.removeChild(ghost);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeholder && placeholder.parentNode) {
|
||||||
|
placeholder.parentNode.removeChild(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLayout(grid) {
|
||||||
|
const pageKey = getPageKey();
|
||||||
|
const cards = grid.querySelectorAll('.card');
|
||||||
|
const order = Array.from(cards).map(card => card.id);
|
||||||
|
localStorage.setItem('blockLayout_' + pageKey, JSON.stringify(order));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLayout(grid) {
|
||||||
|
const pageKey = getPageKey();
|
||||||
|
const saved = localStorage.getItem('blockLayout_' + pageKey);
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const order = JSON.parse(saved);
|
||||||
|
const cards = Array.from(grid.querySelectorAll('.card'));
|
||||||
|
|
||||||
|
// Sort cards according to saved order
|
||||||
|
order.forEach(function(id) {
|
||||||
|
const card = cards.find(c => c.id === id);
|
||||||
|
if (card) {
|
||||||
|
grid.appendChild(card);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load layout:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageKey() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
if (path.includes('/tryouts/')) {
|
||||||
|
return 'tryout';
|
||||||
|
}
|
||||||
|
if (path.includes('/profile')) {
|
||||||
|
return 'profile';
|
||||||
|
}
|
||||||
|
return 'dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize draggable blocks on DOM ready
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Load theme preference
|
// Load theme preference
|
||||||
loadTheme();
|
loadTheme();
|
||||||
|
|
||||||
|
// Initialize draggable blocks
|
||||||
|
initDraggableBlocks();
|
||||||
|
|
||||||
const alerts = document.querySelectorAll('.alert-dismissible');
|
const alerts = document.querySelectorAll('.alert-dismissible');
|
||||||
alerts.forEach(function(alert) {
|
alerts.forEach(function(alert) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
@@ -65,12 +354,3 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Confirm delete actions
|
|
||||||
document.querySelectorAll('[data-confirm]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function(e) {
|
|
||||||
if (!confirm(this.getAttribute('data-confirm'))) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -229,10 +229,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if can_view_calendar %}
|
{% if can_view_calendar %}
|
||||||
<div class="card mt-4">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3><i class="fas fa-futbol"></i> Schedule</h3>
|
<h3><i class="fas fa-futbol"></i> Schedule</h3>
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
@@ -368,7 +367,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="card mt-4">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3><i class="fas fa-star"></i> Evaluation Summary</h3>
|
<h3><i class="fas fa-star"></i> Evaluation Summary</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user