diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc index ee6c872..ef4b7f9 100644 Binary files a/__pycache__/app.cpython-312.pyc and b/__pycache__/app.cpython-312.pyc differ diff --git a/instance/team_tryouts.db b/instance/team_tryouts.db index c56b0f3..3aee324 100644 Binary files a/instance/team_tryouts.db and b/instance/team_tryouts.db differ diff --git a/routes/__pycache__/main.cpython-312.pyc b/routes/__pycache__/main.cpython-312.pyc index efb111b..64de34a 100644 Binary files a/routes/__pycache__/main.cpython-312.pyc and b/routes/__pycache__/main.cpython-312.pyc differ diff --git a/routes/__pycache__/matches.cpython-312.pyc b/routes/__pycache__/matches.cpython-312.pyc index acfa273..bb58558 100644 Binary files a/routes/__pycache__/matches.cpython-312.pyc and b/routes/__pycache__/matches.cpython-312.pyc differ diff --git a/routes/__pycache__/tryouts.cpython-312.pyc b/routes/__pycache__/tryouts.cpython-312.pyc index a4a5ccd..8bbbef8 100644 Binary files a/routes/__pycache__/tryouts.cpython-312.pyc and b/routes/__pycache__/tryouts.cpython-312.pyc differ diff --git a/static/css/style.css b/static/css/style.css index 40ae852..65fda10 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -325,12 +325,30 @@ a:hover { color: var(--primary-dark); } /* Dashboard Grid */ .dashboard-grid { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 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) { .dashboard-grid { grid-template-columns: 1fr; } + .dashboard-grid .card[data-position="right"], + .dashboard-grid .card[data-position="left"] { + grid-column: auto; + } } /* Tables */ @@ -1499,3 +1517,99 @@ a:hover { color: var(--primary-dark); } background: var(--bg-tertiary); 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; +} diff --git a/static/js/main.js b/static/js/main.js index bfb9890..94f4546 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -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 = ''; + 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() { // Load theme preference loadTheme(); + // Initialize draggable blocks + initDraggableBlocks(); + const alerts = document.querySelectorAll('.alert-dismissible'); alerts.forEach(function(alert) { setTimeout(function() { @@ -64,13 +353,4 @@ document.addEventListener('DOMContentLoaded', function() { } }, 5000); }); -}); - -// Confirm delete actions -document.querySelectorAll('[data-confirm]').forEach(function(el) { - el.addEventListener('click', function(e) { - if (!confirm(this.getAttribute('data-confirm'))) { - e.preventDefault(); - } - }); -}); +}); \ No newline at end of file diff --git a/templates/pages/view_tryout.html b/templates/pages/view_tryout.html index f6749a8..845359b 100644 --- a/templates/pages/view_tryout.html +++ b/templates/pages/view_tryout.html @@ -229,189 +229,188 @@ {% endfor %} - - {% if can_view_calendar %} -
-
-

Schedule

- {% if can_edit %} - - Schedule Match - - {% endif %} -
-
- {% if matches %} - -
- {% else %} - -
- -

No matches scheduled yet. Check back later!

+ {% if can_view_calendar %} +
+
+

Schedule

+ {% if can_edit %} + + Schedule Match + + {% endif %}
- {% endif %} - +
+ {% if matches %} + +
+ {% else %} + +
+ +

No matches scheduled yet. Check back later!

+
+ {% endif %} + {% if matches %} - -
- - - - - - - - - - {% if can_edit %} - - {% endif %} - - - - {% for item in match_data %} - {% set m = item.match %} - {% set participants = item.participants %} - - - - - - + + + {% for item in match_data %} + {% set m = item.match %} + {% set participants = item.participants %} + + + + + + + + {% if can_edit %} + + {% endif %} + + {% endfor %} + +
MatchTypeDateParticipantsTimeStatusActions
{{ m.title }} - - {% if m.match_type == 'team_vs_team' %} - Team vs Team - {% elif m.match_type == 'player_vs_player' %} - Player vs Player - {% else %} - Player Scrim - {% endif %} - - {{ m.date.strftime('%m/%d/%Y') }} - {% if m.match_type == 'team_vs_team' %} -
-
- {{ m.team1.name if m.team1 else 'Team 1' }} - {% if participants.team1_players %} -
    - {% for pl in participants.team1_players %} -
  • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
  • - {% endfor %} -
- {% endif %} -
-
vs
-
- {{ m.team2.name if m.team2 else 'Team 2' }} - {% if participants.team2_players %} -
    - {% for pl in participants.team2_players %} -
  • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
  • - {% endfor %} -
- {% endif %} -
-
- {% elif m.match_type == 'player_vs_player' %} -
-
- Team 1 - {% if participants.team1_players %} -
    - {% for pl in participants.team1_players %} -
  • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
  • - {% endfor %} -
- {% endif %} -
-
vs
-
- Team 2 - {% if participants.team2_players %} -
    - {% for pl in participants.team2_players %} -
  • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
  • - {% endfor %} -
- {% endif %} -
-
- {% else %} - {{ participants | join(', ') }} - {% endif %} -
- {% if m.start_time and m.end_time %} - {{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }} - {% else %} - TBD + +
+ + + + + + + + + + {% if can_edit %} + {% endif %} - - - {% if can_edit %} - - {% endif %} - - {% endfor %} - -
MatchTypeDateParticipantsTimeStatusActions - {{ m.status }} - - - Edit - -
+
{{ m.title }} + + {% if m.match_type == 'team_vs_team' %} + Team vs Team + {% elif m.match_type == 'player_vs_player' %} + Player vs Player + {% else %} + Player Scrim + {% endif %} + + {{ m.date.strftime('%m/%d/%Y') }} + {% if m.match_type == 'team_vs_team' %} +
+
+ {{ m.team1.name if m.team1 else 'Team 1' }} + {% if participants.team1_players %} +
    + {% for pl in participants.team1_players %} +
  • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
  • + {% endfor %} +
+ {% endif %} +
+
vs
+
+ {{ m.team2.name if m.team2 else 'Team 2' }} + {% if participants.team2_players %} +
    + {% for pl in participants.team2_players %} +
  • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
  • + {% endfor %} +
+ {% endif %} +
+
+ {% elif m.match_type == 'player_vs_player' %} +
+
+ Team 1 + {% if participants.team1_players %} +
    + {% for pl in participants.team1_players %} +
  • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
  • + {% endfor %} +
+ {% endif %} +
+
vs
+
+ Team 2 + {% if participants.team2_players %} +
    + {% for pl in participants.team2_players %} +
  • {{ pl.name }}{% if pl.position %} {{ pl.position }}{% endif %}
  • + {% endfor %} +
+ {% endif %} +
+
+ {% else %} + {{ participants | join(', ') }} + {% endif %} +
+ {% if m.start_time and m.end_time %} + {{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }} + {% else %} + TBD + {% endif %} + + {{ m.status }} + + + Edit + +
+
+ {% endif %}
- {% endif %}
-
- {% endif %} + {% endif %} -
-
-

Evaluation Summary

-
-
-
- - - - - - - - - - - - - - - - {% for eval in evaluations %} - - - - - - - - - - - - {% else %} - - - - {% endfor %} - -
PlayerEvaluatorSpeedAgilityTechniqueTeamworkAttitudeOverallRecommendation
{{ eval.player.full_name }}{{ eval.evaluator.full_name }}{{ eval.speed_score or '-' }}{{ eval.agility_score or '-' }}{{ eval.technique_score or '-' }}{{ eval.teamwork_score or '-' }}{{ eval.attitude_score or '-' }}{{ eval.overall_score or '-' }}{{ eval.position_recommendation or '-' }}
No evaluations yet.
+
+
+

Evaluation Summary

+
+
+
+ + + + + + + + + + + + + + + + {% for eval in evaluations %} + + + + + + + + + + + + {% else %} + + + + {% endfor %} + +
PlayerEvaluatorSpeedAgilityTechniqueTeamworkAttitudeOverallRecommendation
{{ eval.player.full_name }}{{ eval.evaluator.full_name }}{{ eval.speed_score or '-' }}{{ eval.agility_score or '-' }}{{ eval.technique_score or '-' }}{{ eval.teamwork_score or '-' }}{{ eval.attitude_score or '-' }}{{ eval.overall_score or '-' }}{{ eval.position_recommendation or '-' }}
No evaluations yet.
+
-