Auto commit: 2025-10-24T11:09:54.226Z
This commit is contained in:
parent
9a00d79244
commit
008dda1d41
59
api/add_song_to_lineup.php
Normal file
59
api/add_song_to_lineup.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
// Get the posted data
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$data || !isset($data['lineup_id']) || !isset($data['song_id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid input.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lineup_id = $data['lineup_id'];
|
||||||
|
$song_id = $data['song_id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Check if the song is already in the lineup
|
||||||
|
$stmt = $pdo->prepare("SELECT 1 FROM lineup_songs WHERE lineup_id = ? AND song_id = ?");
|
||||||
|
$stmt->execute([$lineup_id, $song_id]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
http_response_code(409); // 409 Conflict
|
||||||
|
echo json_encode(['success' => false, 'message' => 'השיר כבר קיים בליינאפ.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current number of songs in the lineup to determine the new order.
|
||||||
|
// This is much faster than calculating MAX(song_order).
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) as song_count FROM lineup_songs WHERE lineup_id = ?");
|
||||||
|
$stmt->execute([$lineup_id]);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$new_order = $result ? (int)$result['song_count'] : 0;
|
||||||
|
|
||||||
|
|
||||||
|
// Insert the new song
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO lineup_songs (lineup_id, song_id, song_order) VALUES (?, ?, ?)");
|
||||||
|
|
||||||
|
if ($stmt->execute([$lineup_id, $song_id, $new_order])) {
|
||||||
|
// Fetch the added song details to return to the client
|
||||||
|
$song_stmt = $pdo->prepare("SELECT * FROM songs WHERE id = ?");
|
||||||
|
$song_stmt->execute([$song_id]);
|
||||||
|
$song = $song_stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'השיר נוסף בהצלחה!', 'song' => $song]);
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'לא ניתן היה להוסיף את השיר.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Log error to a file for debugging
|
||||||
|
error_log("Add song to lineup failed: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
// Return a more specific error message for debugging
|
||||||
|
echo json_encode(['success' => false, 'message' => 'שגיאת שרת: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
34
api/lineups_api.php
Normal file
34
api/lineups_api.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$response = ['success' => false, 'error' => 'Invalid request'];
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (isset($data['action'])) {
|
||||||
|
switch ($data['action']) {
|
||||||
|
case 'create':
|
||||||
|
if (!empty($data['name'])) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO lineups (name) VALUES (?)");
|
||||||
|
$stmt->execute([$data['name']]);
|
||||||
|
$response = ['success' => true, 'lineup_id' => $pdo->lastInsertId()];
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$response['error'] = 'Database error: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$response['error'] = 'Lineup name is required.';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$response['error'] = 'Unknown action.';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
28
api/remove_song_from_lineup.php
Normal file
28
api/remove_song_from_lineup.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
require_once '../db/config.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$lineup_id = $data['lineup_id'] ?? null;
|
||||||
|
$song_id = $data['song_id'] ?? null;
|
||||||
|
|
||||||
|
if ($lineup_id && $song_id) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM lineup_songs WHERE lineup_id = ? AND song_id = ?");
|
||||||
|
$stmt->execute([$lineup_id, $song_id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid input.']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Method not allowed.']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
23
api/search_songs.php
Normal file
23
api/search_songs.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
require_once '../db/config.php';
|
||||||
|
|
||||||
|
// --- Logic ---
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Fetch songs with search functionality
|
||||||
|
$search = $_GET['q'] ?? $_GET['search'] ?? '';
|
||||||
|
$sql = "SELECT * FROM songs";
|
||||||
|
$params = [];
|
||||||
|
if (!empty($search)) {
|
||||||
|
$sql .= " WHERE name LIKE ? OR artist LIKE ? OR bpm LIKE ? OR song_key LIKE ? OR notes LIKE ? OR tags LIKE ?";
|
||||||
|
$searchTerm = "%{$search}%";
|
||||||
|
$params = [$searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm];
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY id ASC";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$songs = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// --- Presentation ---
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($songs);
|
||||||
118
assets/js/lineup_details_page.js
Normal file
118
assets/js/lineup_details_page.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const lineupId = document.getElementById('lineup-id').value;
|
||||||
|
const searchInput = document.getElementById('song-search-input');
|
||||||
|
const searchResultsContainer = document.getElementById('search-results');
|
||||||
|
const lineupSongList = document.getElementById('lineup-song-list');
|
||||||
|
|
||||||
|
// 1. Search for songs
|
||||||
|
searchInput.addEventListener('keyup', function () {
|
||||||
|
const query = this.value;
|
||||||
|
|
||||||
|
if (query.length < 2) {
|
||||||
|
searchResultsContainer.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('api/search_songs.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: `query=${encodeURIComponent(query)}&lineup_id=${lineupId}`
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(songs => {
|
||||||
|
searchResultsContainer.innerHTML = '';
|
||||||
|
if (songs.length > 0) {
|
||||||
|
const list = document.createElement('ul');
|
||||||
|
list.className = 'list-group';
|
||||||
|
songs.forEach(song => {
|
||||||
|
const listItem = document.createElement('li');
|
||||||
|
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
|
||||||
|
listItem.innerHTML = `
|
||||||
|
<span><strong>${song.artist}</strong> - ${song.name}</span>
|
||||||
|
<button class="btn btn-sm btn-primary add-song-btn" data-song-id="${song.id}">הוסף</button>
|
||||||
|
`;
|
||||||
|
list.appendChild(listItem);
|
||||||
|
});
|
||||||
|
searchResultsContainer.appendChild(list);
|
||||||
|
} else {
|
||||||
|
searchResultsContainer.innerHTML = '<p class="text-muted">לא נמצאו שירים תואמים.</p>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error searching for songs:', error);
|
||||||
|
searchResultsContainer.innerHTML = '<p class="text-danger">אירעה שגיאה בחיפוש.</p>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Add a song to the lineup
|
||||||
|
searchResultsContainer.addEventListener('click', function (e) {
|
||||||
|
if (e.target && e.target.classList.contains('add-song-btn')) {
|
||||||
|
const songId = e.target.dataset.songId;
|
||||||
|
|
||||||
|
fetch('api/add_song_to_lineup.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: `song_id=${songId}&lineup_id=${lineupId}`
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
// Reload the page to see the updated list. It's simple and reliable.
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('ההוספה נכשלה: ' + (result.message || 'שגיאה לא ידועה'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error adding song:', error);
|
||||||
|
alert('אירעה שגיאה קריטית בעת ההוספה.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Remove a song from the lineup
|
||||||
|
lineupSongList.addEventListener('click', function (e) {
|
||||||
|
if (e.target && e.target.classList.contains('remove-song-btn')) {
|
||||||
|
const songId = e.target.dataset.songId;
|
||||||
|
|
||||||
|
if (!confirm('האם אתה בטוח שברצונך להסיר את השיר הזה?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('api/remove_song_from_lineup.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: `song_id=${songId}&lineup_id=${lineupId}`
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
// Remove the song from the list in the UI
|
||||||
|
e.target.closest('li.list-group-item').remove();
|
||||||
|
|
||||||
|
// If the list is now empty, show the "empty" message
|
||||||
|
if (lineupSongList.children.length === 0) {
|
||||||
|
const emptyMessage = document.createElement('li');
|
||||||
|
emptyMessage.id = 'empty-lineup-message';
|
||||||
|
emptyMessage.className = 'list-group-item text-center text-muted';
|
||||||
|
emptyMessage.textContent = 'אין עדיין שירים בליינאפ זה.';
|
||||||
|
lineupSongList.appendChild(emptyMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('ההסרה נכשלה: ' + (result.message || 'שגיאה לא ידועה'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error removing song:', error);
|
||||||
|
alert('אירעה שגיאה קריטית בעת ההסרה.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,81 +1,41 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Initialize toast notifications
|
// Handle Create Lineup form submission
|
||||||
const toastEl = document.getElementById('notificationToast');
|
const createLineupForm = document.getElementById('createLineupForm');
|
||||||
if (toastEl) {
|
if (createLineupForm) {
|
||||||
const toast = new bootstrap.Toast(toastEl, { delay: 3000 });
|
createLineupForm.addEventListener('submit', function(event) {
|
||||||
toast.show();
|
event.preventDefault();
|
||||||
}
|
const lineupName = document.getElementById('lineupName').value;
|
||||||
|
// The submit button is outside the form, so we find it in the modal footer
|
||||||
|
const createButton = this.closest('.modal-content').querySelector('.modal-footer button[type="submit"]');
|
||||||
|
const originalButtonText = createButton.innerHTML;
|
||||||
|
|
||||||
const songModalEl = document.getElementById('songModal');
|
createButton.disabled = true;
|
||||||
if (!songModalEl) return;
|
createButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> יוצר...';
|
||||||
|
|
||||||
const songModal = new bootstrap.Modal(songModalEl);
|
fetch('api/lineups_api.php', {
|
||||||
const modalTitle = songModalEl.querySelector('.modal-title');
|
method: 'POST',
|
||||||
const songForm = document.getElementById('songForm');
|
headers: {
|
||||||
const actionInput = document.getElementById('action');
|
'Content-Type': 'application/json',
|
||||||
const songIdInput = document.getElementById('song_id');
|
},
|
||||||
|
body: JSON.stringify({ action: 'create', name: lineupName }),
|
||||||
// Function to reset the modal to its "Add Song" state
|
})
|
||||||
const resetModal = () => {
|
.then(response => response.json())
|
||||||
songForm.reset();
|
.then(data => {
|
||||||
actionInput.value = 'create';
|
if (data.success) {
|
||||||
songIdInput.value = '';
|
window.location.reload(); // Reload to see the new lineup
|
||||||
modalTitle.textContent = 'הוספת שיר חדש';
|
} else {
|
||||||
};
|
alert('שגיאה ביצירת הליינאפ: ' + data.error);
|
||||||
|
}
|
||||||
// Handle clicks on edit buttons
|
})
|
||||||
document.querySelectorAll('.edit-btn').forEach(button => {
|
.catch(error => {
|
||||||
button.addEventListener('click', function () {
|
console.error('Error:', error);
|
||||||
const song = JSON.parse(this.dataset.song);
|
alert('אירעה שגיאה בלתי צפויה.');
|
||||||
|
})
|
||||||
resetModal(); // Start with a clean slate
|
.finally(() => {
|
||||||
|
// This might not be reached if the page reloads, but it's good practice
|
||||||
actionInput.value = 'update';
|
createButton.disabled = false;
|
||||||
modalTitle.textContent = 'עריכת שיר';
|
createButton.innerHTML = originalButtonText;
|
||||||
songIdInput.value = song.id;
|
});
|
||||||
|
|
||||||
document.getElementById('name').value = song.name;
|
|
||||||
document.getElementById('bpm').value = song.bpm;
|
|
||||||
|
|
||||||
if (song.song_key && song.song_key.trim() !== '') {
|
|
||||||
const keyParts = song.song_key.split(' ');
|
|
||||||
document.getElementById('key_note').value = keyParts[0];
|
|
||||||
document.getElementById('key_scale').value = keyParts[1] || 'Major';
|
|
||||||
} else {
|
|
||||||
document.getElementById('key_note').value = '';
|
|
||||||
document.getElementById('key_scale').value = 'Major';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('notes').value = song.notes;
|
|
||||||
document.getElementById('tags').value = song.tags;
|
|
||||||
|
|
||||||
if (song.duration_seconds) {
|
|
||||||
const minutes = Math.floor(song.duration_seconds / 60);
|
|
||||||
const seconds = song.duration_seconds % 60;
|
|
||||||
document.getElementById('duration_minutes').value = minutes;
|
|
||||||
document.getElementById('duration_seconds').value = seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
songModal.show();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle click on the link to add the first song
|
|
||||||
const addSongBtnLink = document.getElementById('addSongBtnLink');
|
|
||||||
if(addSongBtnLink) {
|
|
||||||
addSongBtnLink.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
resetModal();
|
|
||||||
songModal.show();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// Reset the modal form when it's opened via the main "Add Song" button
|
|
||||||
// The main button works via data-attributes, so we just need to hook into the event
|
|
||||||
songModalEl.addEventListener('show.bs.modal', function (event) {
|
|
||||||
// If the trigger was NOT an edit button, reset the form for adding.
|
|
||||||
if (event.relatedTarget && !event.relatedTarget.classList.contains('edit-btn')) {
|
|
||||||
resetModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
178
assets/js/songs_page.js
Normal file
178
assets/js/songs_page.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Live search for songs page
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const clearSearchBtn = document.getElementById('clearSearchBtn');
|
||||||
|
const tableBody = document.getElementById('songsTableBody');
|
||||||
|
const originalNoSongsRow = document.getElementById('noSongsRow')?.cloneNode(true);
|
||||||
|
|
||||||
|
if (searchInput && tableBody) {
|
||||||
|
// Function to format duration from seconds to MM:SS
|
||||||
|
const formatDuration = (seconds) => {
|
||||||
|
if (seconds === null || seconds < 0) return '00:00';
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to render table rows
|
||||||
|
const renderTable = (songs, searchTerm) => {
|
||||||
|
tableBody.innerHTML = ''; // Clear existing rows
|
||||||
|
|
||||||
|
if (songs.length === 0) {
|
||||||
|
const noResultsRow = document.createElement('tr');
|
||||||
|
noResultsRow.innerHTML = `<td colspan="9" class="text-center text-muted py-4">לא נמצאו שירים התואמים את החיפוש "${searchTerm}".</td>`;
|
||||||
|
if (searchTerm === '') {
|
||||||
|
if(originalNoSongsRow){
|
||||||
|
tableBody.appendChild(originalNoSongsRow);
|
||||||
|
} else {
|
||||||
|
noResultsRow.innerHTML = `<td colspan="9" class="text-center text-muted py-4">עדיין אין שירים במאגר. <a href="#" onclick="document.getElementById('addSongBtn').click()">הוסף את השיר הראשון שלך!</a></td>`;
|
||||||
|
tableBody.appendChild(noResultsRow);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tableBody.appendChild(noResultsRow);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
songs.forEach((song, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const tagsHtml = song.tags.split(',').map(tag => {
|
||||||
|
const trimmedTag = tag.trim();
|
||||||
|
return trimmedTag ? `<span class="badge bg-secondary bg-opacity-25 text-dark-emphasis">${escapeHTML(trimmedTag)}</span>` : '';
|
||||||
|
}).join(' ');
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td>${escapeHTML(song.artist)}</td>
|
||||||
|
<td class="fw-bold">${escapeHTML(song.name)}</td>
|
||||||
|
<td>${escapeHTML(song.bpm)}</td>
|
||||||
|
<td>${escapeHTML(song.song_key)}</td>
|
||||||
|
<td>${formatDuration(song.duration_seconds)}</td>
|
||||||
|
<td>${tagsHtml}</td>
|
||||||
|
<td>${escapeHTML(song.notes)}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary edit-btn" data-song='${escapeHTML(JSON.stringify(song))}' data-bs-toggle="modal" data-bs-target="#songModal">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
<form action="songs.php" method="POST" class="d-inline" onsubmit="return confirm('האם אתה בטוח שברצונך למחוק את השיר?');">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="id" value="${song.id}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debounce function to limit API calls
|
||||||
|
let debounceTimeout;
|
||||||
|
const handleSearch = () => {
|
||||||
|
const searchTerm = searchInput.value.trim();
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
|
||||||
|
clearSearchBtn.style.display = searchTerm ? 'block' : 'none';
|
||||||
|
|
||||||
|
debounceTimeout = setTimeout(() => {
|
||||||
|
fetch(`api/search_songs.php?search=${encodeURIComponent(searchTerm)}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(songs => {
|
||||||
|
renderTable(songs, searchTerm);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching search results:', error));
|
||||||
|
}, 300); // 300ms delay
|
||||||
|
};
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', handleSearch);
|
||||||
|
|
||||||
|
clearSearchBtn.addEventListener('click', () => {
|
||||||
|
searchInput.value = '';
|
||||||
|
handleSearch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHTML(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
return String(str).replace(/[&<>'"/]/g, function (s) {
|
||||||
|
return {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
'/': '/'
|
||||||
|
}[s];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const songModalEl = document.getElementById('songModal');
|
||||||
|
if (songModalEl) {
|
||||||
|
const songModal = new bootstrap.Modal(songModalEl);
|
||||||
|
const modalTitle = songModalEl.querySelector('.modal-title');
|
||||||
|
const songForm = document.getElementById('songForm');
|
||||||
|
const actionInput = document.getElementById('action');
|
||||||
|
const songIdInput = document.getElementById('song_id');
|
||||||
|
|
||||||
|
// Function to reset the modal to its "Add Song" state
|
||||||
|
const resetModal = () => {
|
||||||
|
songForm.reset();
|
||||||
|
actionInput.value = 'create';
|
||||||
|
songIdInput.value = '';
|
||||||
|
modalTitle.textContent = 'הוספת שיר חדש';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use event delegation for edit buttons since they are dynamically loaded
|
||||||
|
document.body.addEventListener('click', function(event) {
|
||||||
|
const editBtn = event.target.closest('.edit-btn');
|
||||||
|
if (editBtn) {
|
||||||
|
const song = JSON.parse(editBtn.dataset.song);
|
||||||
|
|
||||||
|
resetModal(); // Start with a clean slate
|
||||||
|
|
||||||
|
actionInput.value = 'update';
|
||||||
|
modalTitle.textContent = 'עריכת שיר';
|
||||||
|
songIdInput.value = song.id;
|
||||||
|
|
||||||
|
document.getElementById('name').value = song.name;
|
||||||
|
document.getElementById('artist').value = song.artist;
|
||||||
|
document.getElementById('bpm').value = song.bpm;
|
||||||
|
|
||||||
|
if (song.song_key && song.song_key.trim() !== '') {
|
||||||
|
const keyParts = song.song_key.split(' ');
|
||||||
|
document.getElementById('key_note').value = keyParts[0];
|
||||||
|
document.getElementById('key_scale').value = keyParts[1] || 'Major';
|
||||||
|
} else {
|
||||||
|
document.getElementById('key_note').value = '';
|
||||||
|
document.getElementById('key_scale').value = 'Major';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('notes').value = song.notes;
|
||||||
|
document.getElementById('tags').value = song.tags;
|
||||||
|
|
||||||
|
if (song.duration_seconds) {
|
||||||
|
const minutes = Math.floor(song.duration_seconds / 60);
|
||||||
|
const seconds = song.duration_seconds % 60;
|
||||||
|
document.getElementById('duration_minutes').value = minutes;
|
||||||
|
document.getElementById('duration_seconds').value = seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
songModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSongLink = event.target.closest('#addSongBtnLink');
|
||||||
|
if(addSongLink){
|
||||||
|
event.preventDefault();
|
||||||
|
resetModal();
|
||||||
|
songModal.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset the modal form when it's opened via the main "Add Song" button
|
||||||
|
songModalEl.addEventListener('show.bs.modal', function (event) {
|
||||||
|
if (event.relatedTarget && !event.relatedTarget.classList.contains('edit-btn')) {
|
||||||
|
resetModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
BIN
assets/pasted-20251024-093024-ed968ccb.png
Normal file
BIN
assets/pasted-20251024-093024-ed968ccb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/pasted-20251024-093716-af880ec1.png
Normal file
BIN
assets/pasted-20251024-093716-af880ec1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
assets/pasted-20251024-094241-a54f51c9.png
Normal file
BIN
assets/pasted-20251024-094241-a54f51c9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/pasted-20251024-100423-adeccb17.png
Normal file
BIN
assets/pasted-20251024-100423-adeccb17.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/pasted-20251024-101729-6227f5cd.png
Normal file
BIN
assets/pasted-20251024-101729-6227f5cd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/pasted-20251024-105800-5649a69d.png
Normal file
BIN
assets/pasted-20251024-105800-5649a69d.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'config.php';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Connect without a database to create it
|
|
||||||
$pdo_admin = new PDO('mysql:host='.DB_HOST, DB_USER, DB_PASS, [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
]);
|
|
||||||
$pdo_admin->exec("CREATE DATABASE IF NOT EXISTS `".DB_NAME."` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
|
||||||
// echo "Database '".DB_NAME."' created or already exists.\n";
|
|
||||||
|
|
||||||
// 2. Connect to the specific database to create tables
|
|
||||||
$pdo = db();
|
|
||||||
if ($pdo === null) {
|
|
||||||
throw new Exception("Failed to connect to the database. The db() function returned null.");
|
|
||||||
}
|
|
||||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
|
|
||||||
$sql = "
|
|
||||||
CREATE TABLE IF NOT EXISTS songs (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
bpm INT,
|
|
||||||
song_key VARCHAR(50),
|
|
||||||
duration_seconds INT,
|
|
||||||
notes TEXT,
|
|
||||||
tags VARCHAR(255),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
||||||
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;";
|
|
||||||
|
|
||||||
$pdo->exec($sql);
|
|
||||||
// echo "Table 'songs' created successfully (if it didn\'t exist).\n";
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
http_response_code(500);
|
|
||||||
die("DB ERROR: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
26
db/migrations/001_initial_schema.sql
Normal file
26
db/migrations/001_initial_schema.sql
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
-- Initial Schema for Lineup Application
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `lineups` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`name` VARCHAR(255) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `songs` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`title` VARCHAR(255) NOT NULL,
|
||||||
|
`artist` VARCHAR(255) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `lineup_songs` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`lineup_id` INT NOT NULL,
|
||||||
|
`song_id` INT NOT NULL,
|
||||||
|
`display_order` INT NOT NULL DEFAULT 0,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`lineup_id`) REFERENCES `lineups`(`id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`song_id`) REFERENCES `songs`(`id`) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY `lineup_song_unique` (`lineup_id`, `song_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
7
db/migrations/002_add_indexes.sql
Normal file
7
db/migrations/002_add_indexes.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-- Add indexes to lineup_songs for faster lookups
|
||||||
|
ALTER TABLE `lineup_songs` ADD INDEX `idx_lineup_id` (`lineup_id`);
|
||||||
|
ALTER TABLE `lineup_songs` ADD INDEX `idx_song_id` (`song_id`);
|
||||||
|
|
||||||
|
-- Add a unique constraint to prevent adding the same song to a lineup twice
|
||||||
|
-- This also creates an index
|
||||||
|
ALTER TABLE `lineup_songs` ADD UNIQUE `unique_lineup_song` (`lineup_id`, `song_id`);
|
||||||
2
db/migrations/003_add_display_order_column.sql
Normal file
2
db/migrations/003_add_display_order_column.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add display_order column to lineup_songs table
|
||||||
|
ALTER TABLE `lineup_songs` ADD COLUMN `display_order` INT NOT NULL DEFAULT 0 AFTER `song_id`;
|
||||||
2
db/migrations/004_add_order_index.sql
Normal file
2
db/migrations/004_add_order_index.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add index for display_order to speed up sorting
|
||||||
|
ALTER TABLE `lineup_songs` ADD INDEX `idx_display_order` (`display_order`);
|
||||||
1
db/migrations/005_add_artist_to_songs.sql
Normal file
1
db/migrations/005_add_artist_to_songs.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `songs` ADD `artist` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL AFTER `name`;
|
||||||
2
db/migrations/006_add_song_order_index.sql
Normal file
2
db/migrations/006_add_song_order_index.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add index for song_order to speed up sorting
|
||||||
|
ALTER TABLE `lineup_songs` ADD INDEX `idx_song_order` (`song_order`);
|
||||||
44
debug_songs.php
Normal file
44
debug_songs.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
echo '<h1>מצב מאגר השירים</h1>';
|
||||||
|
echo '<p>בעמוד זה מוצגת רשימת השירים כפי שהיא שמורה כרגע במסד הנתונים.</p>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$stmt = $pdo->query("SELECT * FROM songs ORDER BY id ASC");
|
||||||
|
$songs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($songs)) {
|
||||||
|
echo '<div style="border: 1px solid #ccc; padding: 15px; background-color: #f0f0f0;"><strong>המאגר ריק.</strong> לא נמצאו שירים.</div>';
|
||||||
|
} else {
|
||||||
|
echo '<table border="1" cellpadding="5" cellspacing="0" style="width: 100%; border-collapse: collapse;">';
|
||||||
|
echo '<thead style="background-color: #f2f2f2;"><tr><th>ID</th><th>Name</th><th>Artist</th><th>BPM</th><th>Key</th><th>Duration</th><th>Tags</th><th>Notes</th></tr></thead>';
|
||||||
|
echo '<tbody>';
|
||||||
|
foreach ($songs as $song) {
|
||||||
|
echo '<tr>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['id']) . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['name']) . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['artist'] ?? 'N/A') . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['bpm'] ?? 'N/A') . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['song_key'] ?? 'N/A') . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['duration_seconds'] ?? 'N/A') . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['tags'] ?? 'N/A') . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['notes'] ?? 'N/A') . '</td>';
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
echo '</tbody></table>';
|
||||||
|
echo '<p style="margin-top: 15px;"><strong>סה"כ שירים: ' . count($songs) . '</strong></p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo '<div style="border: 1px solid red; padding: 15px; background-color: #ffebeb; color: red;">';
|
||||||
|
echo '<strong>שגיאה חמורה!</strong><br>';
|
||||||
|
echo 'לא ניתן היה להתחבר למסד הנתונים או לשלוף את המידע.<br>';
|
||||||
|
echo 'פרטי השגיאה: ' . $e->getMessage();
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
@ -28,14 +28,15 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<?php $pageName = basename($_SERVER['PHP_SELF'], '.php'); ?>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="index.php">בית</a>
|
<a class="nav-link <?php echo ($pageName == 'index') ? 'active' : ''; ?>" <?php echo ($pageName == 'index') ? 'aria-current="page"' : ''; ?> href="index.php">בית</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" aria-current="page" href="songs.php">מאגר שירים</a>
|
<a class="nav-link <?php echo ($pageName == 'songs') ? 'active' : ''; ?>" <?php echo ($pageName == 'songs') ? 'aria-current="page"' : ''; ?> href="songs.php">מאגר שירים</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link disabled" href="#">ליינאפים</a>
|
<a class="nav-link <?php echo ($pageName == 'lineups') ? 'active' : ''; ?>" <?php echo ($pageName == 'lineups') ? 'aria-current="page"' : ''; ?> href="lineups.php">ליינאפים</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
<?php include 'includes/header.php'; ?>
|
<?php
|
||||||
|
$pageName = 'index';
|
||||||
|
include 'includes/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="display-4 fw-bold">ברוכים הבאים ל-Ari Stage</h1>
|
<h1 class="display-4 fw-bold">ברוכים הבאים ל-Ari Stage</h1>
|
||||||
|
|||||||
95
lineup_details.php
Normal file
95
lineup_details.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
$pageName = 'פרטי ליינאפ';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
// Check if lineup ID is provided
|
||||||
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
|
echo '<div class="container mt-4"><div class="alert alert-danger">מזהה ליינאפ לא תקין.</div></div>';
|
||||||
|
require_once 'includes/footer.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lineup_id = intval($_GET['id']);
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
// Fetch lineup details
|
||||||
|
$stmt = $db->prepare("SELECT * FROM lineups WHERE id = ?");
|
||||||
|
$stmt->execute([$lineup_id]);
|
||||||
|
$lineup = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$lineup) {
|
||||||
|
echo '<div class="container mt-4"><div class="alert alert-danger">הליינאפ לא נמצא.</div></div>';
|
||||||
|
require_once 'includes/footer.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch songs for this lineup (to be implemented)
|
||||||
|
$songs_stmt = $db->prepare("
|
||||||
|
SELECT s.* FROM songs s
|
||||||
|
JOIN lineup_songs ls ON s.id = ls.song_id
|
||||||
|
WHERE ls.lineup_id = ?
|
||||||
|
ORDER BY ls.song_order ASC
|
||||||
|
");
|
||||||
|
$songs_stmt->execute([$lineup_id]);
|
||||||
|
$songs = $songs_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><?php echo htmlspecialchars($lineup['name']); ?></h1>
|
||||||
|
<a href="lineups.php" class="btn btn-outline-secondary">חזרה לרשימת הליינאפים</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Add Songs -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h3>הוספת שירים</h3>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="song-search-input" class="form-label">חפש שיר</label>
|
||||||
|
<input type="text" class="form-control" id="song-search-input" placeholder="הקלד שם שיר או אמן...">
|
||||||
|
</div>
|
||||||
|
<div id="search-results" style="max-height: 300px; overflow-y: auto;">
|
||||||
|
<!-- Search results will appear here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Song List -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h3>שירים בליינאפ</h3>
|
||||||
|
<div id="lineup-songs-container">
|
||||||
|
<ul class="list-group" id="lineup-song-list">
|
||||||
|
<?php if (empty($songs)): ?>
|
||||||
|
<li id="empty-lineup-message" class="list-group-item text-center text-muted">
|
||||||
|
אין עדיין שירים בליינאפ זה.
|
||||||
|
</li>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($songs as $song): ?>
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center" data-song-id="<?php echo $song['id']; ?>">
|
||||||
|
<span>
|
||||||
|
<i class="fas fa-grip-vertical me-2"></i>
|
||||||
|
<strong><?php echo htmlspecialchars($song['artist']); ?></strong> - <?php echo htmlspecialchars($song['name']); ?>
|
||||||
|
</span>
|
||||||
|
<button class="btn btn-sm btn-outline-danger remove-song-btn" data-song-id="<?php echo $song['id']; ?>" data-lineup-id="<?php echo $lineup_id; ?>">הסר</button>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" id="lineup-id" value="<?php echo $lineup_id; ?>">
|
||||||
|
|
||||||
|
<script src="assets/js/lineup_details_page.js?v=<?php echo time(); ?>"></script>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once 'includes/footer.php';
|
||||||
|
?>
|
||||||
72
lineups.php
Normal file
72
lineups.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
$pageTitle = 'הליינאפים שלי';
|
||||||
|
$pageName = 'lineups';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
require_once __DIR__ . '/includes/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>הליינאפים שלי</h1>
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createLineupModal">צור ליינאפ חדש</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->query("SELECT * FROM lineups ORDER BY created_at DESC");
|
||||||
|
$lineups = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Handle DB error
|
||||||
|
$lineups = [];
|
||||||
|
echo "<div class='alert alert-danger'>שגיאה בטעינת הליינאפים: " . htmlspecialchars($e->getMessage()) . "</div>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if (empty($lineups)): ?>
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-music-note-list" style="font-size: 3rem;"></i>
|
||||||
|
<p class="mt-3">עדיין לא יצרת ליינאפים.</p>
|
||||||
|
<p>לחץ על "צור ליינאפ חדש" כדי להתחיל.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="list-group">
|
||||||
|
<?php foreach ($lineups as $lineup): ?>
|
||||||
|
<a href="lineup_details.php?id=<?php echo $lineup['id']; ?>" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1"><?php echo htmlspecialchars($lineup['name']); ?></h5>
|
||||||
|
<small class="text-muted">נוצר ב: <?php echo date('d/m/Y', strtotime($lineup['created_at'])); ?></small>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Lineup Modal -->
|
||||||
|
<div class="modal fade" id="createLineupModal" tabindex="-1" aria-labelledby="createLineupModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="createLineupModalLabel">יצירת ליינאפ חדש</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="createLineupForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="lineupName" class="form-label">שם הליינאפ</label>
|
||||||
|
<input type="text" class="form-control" id="lineupName" name="lineupName" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">סגור</button>
|
||||||
|
<button type="submit" form="createLineupForm" class="btn btn-primary">צור</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||||
77
migrate.php
Normal file
77
migrate.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
echo "Starting database migration...\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// 1. Create migrations tracking table if it doesn't exist
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS `migrations` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`migration_file` VARCHAR(255) NOT NULL,
|
||||||
|
`applied_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY `migration_file_unique` (`migration_file`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||||
|
echo "1. 'migrations' table is ready.\n";
|
||||||
|
|
||||||
|
// 2. Get all migrations that have already been run
|
||||||
|
$stmt = $pdo->query("SELECT `migration_file` FROM `migrations`");
|
||||||
|
$applied_migrations = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
echo "2. Found " . count($applied_migrations) . " applied migrations.\n";
|
||||||
|
|
||||||
|
// 3. Find all migration files on disk
|
||||||
|
$migration_files = glob('db/migrations/*.sql');
|
||||||
|
sort($migration_files);
|
||||||
|
echo "3. Found " . count($migration_files) . " migration files on disk.\n";
|
||||||
|
|
||||||
|
|
||||||
|
// 4. Apply pending migrations
|
||||||
|
$migrations_applied_count = 0;
|
||||||
|
foreach ($migration_files as $file) {
|
||||||
|
$basename = basename($file);
|
||||||
|
if (in_array($basename, $applied_migrations)) {
|
||||||
|
echo "- Skipping already applied migration: {$basename}\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "+ Applying new migration: {$basename}...\n";
|
||||||
|
$sql = file_get_contents($file);
|
||||||
|
if (empty(trim($sql))) {
|
||||||
|
echo " ...file is empty, skipping.\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$pdo->exec($sql);
|
||||||
|
|
||||||
|
$insert_stmt = $pdo->prepare("INSERT INTO `migrations` (migration_file) VALUES (?)");
|
||||||
|
$insert_stmt->execute([$basename]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo " ...SUCCESS!\n";
|
||||||
|
$migrations_applied_count++;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
echo " ...ERROR: " . $e->getMessage() . "\n";
|
||||||
|
echo "\nMigration failed. No changes were made to the database schema.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($migrations_applied_count > 0) {
|
||||||
|
echo "\nFinished. Applied {" . $migrations_applied_count . "} new migrations successfully.\n";
|
||||||
|
} else {
|
||||||
|
echo "\nFinished. Database is already up to date.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "A critical error occurred: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
71
songs.php
71
songs.php
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
$pageName = 'songs';
|
||||||
session_start();
|
session_start();
|
||||||
require_once 'db/config.php';
|
require_once 'db/config.php';
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (empty($name)) {
|
if (empty($name)) {
|
||||||
throw new Exception("שם השיר הוא שדה חובה.");
|
throw new Exception("שם השיר הוא שדה חובה.");
|
||||||
}
|
}
|
||||||
|
$artist = trim($_POST['artist'] ?? '');
|
||||||
|
|
||||||
$bpm = !empty($_POST['bpm']) ? (int)$_POST['bpm'] : null;
|
$bpm = !empty($_POST['bpm']) ? (int)$_POST['bpm'] : null;
|
||||||
$key_note = $_POST['key_note'] ?? '';
|
$key_note = $_POST['key_note'] ?? '';
|
||||||
@ -29,14 +31,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$duration_seconds = ($minutes * 60) + $seconds;
|
$duration_seconds = ($minutes * 60) + $seconds;
|
||||||
|
|
||||||
if ($action === 'create') {
|
if ($action === 'create') {
|
||||||
$stmt = $pdo->prepare("INSERT INTO songs (name, bpm, song_key, duration_seconds, notes, tags) VALUES (?, ?, ?, ?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO songs (name, artist, bpm, song_key, duration_seconds, notes, tags) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([$name, $bpm, $song_key, $duration_seconds, $notes, $tags]);
|
$stmt->execute([$name, $artist, $bpm, $song_key, $duration_seconds, $notes, $tags]);
|
||||||
$_SESSION['notification'] = ['message' => 'השיר נוצר בהצלחה!', 'type' => 'success'];
|
$_SESSION['notification'] = ['message' => 'השיר נוצר בהצלחה!', 'type' => 'success'];
|
||||||
} else { // update
|
} else { // update
|
||||||
$id = (int)($_POST['id'] ?? 0);
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
if ($id > 0) {
|
if ($id > 0) {
|
||||||
$stmt = $pdo->prepare("UPDATE songs SET name=?, bpm=?, song_key=?, duration_seconds=?, notes=?, tags=? WHERE id=?");
|
$stmt = $pdo->prepare("UPDATE songs SET name=?, artist=?, bpm=?, song_key=?, duration_seconds=?, notes=?, tags=? WHERE id=?");
|
||||||
$stmt->execute([$name, $bpm, $song_key, $duration_seconds, $notes, $tags, $id]);
|
$stmt->execute([$name, $artist, $bpm, $song_key, $duration_seconds, $notes, $tags, $id]);
|
||||||
$_SESSION['notification'] = ['message' => 'השיר עודכן בהצלחה!', 'type' => 'success'];
|
$_SESSION['notification'] = ['message' => 'השיר עודכן בהצלחה!', 'type' => 'success'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,8 +66,19 @@ if (isset($_SESSION['notification'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Fetch all songs to display
|
// Fetch songs with search functionality
|
||||||
$songs = $pdo->query("SELECT * FROM songs ORDER BY name ASC")->fetchAll();
|
$search = $_GET['search'] ?? '';
|
||||||
|
$sql = "SELECT * FROM songs";
|
||||||
|
$params = [];
|
||||||
|
if (!empty($search)) {
|
||||||
|
$sql .= " WHERE name LIKE ? OR artist LIKE ? OR bpm LIKE ? OR song_key LIKE ? OR notes LIKE ? OR tags LIKE ?";
|
||||||
|
$searchTerm = "%{$search}%";
|
||||||
|
$params = [$searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm];
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY id ASC";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$songs = $stmt->fetchAll();
|
||||||
|
|
||||||
function format_duration($seconds) {
|
function format_duration($seconds) {
|
||||||
if ($seconds === null || $seconds < 0) return '00:00';
|
if ($seconds === null || $seconds < 0) return '00:00';
|
||||||
@ -98,28 +111,50 @@ include 'includes/header.php';
|
|||||||
<button id="addSongBtn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#songModal"><i class="bi bi-plus-circle me-2"></i>הוסף שיר חדש</button>
|
<button id="addSongBtn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#songModal"><i class="bi bi-plus-circle me-2"></i>הוסף שיר חדש</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<!-- Search Form -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||||
|
<input type="search" id="searchInput" class="form-control" placeholder="חיפוש לפי שם, סולם, תג..." value="<?php echo htmlspecialchars($search); ?>">
|
||||||
|
<button id="clearSearchBtn" class="btn btn-outline-secondary" type="button" style="display: none;"><i class="bi bi-x-lg"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="songsListContainer" class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>שם האמן</th>
|
||||||
<th>שם השיר</th>
|
<th>שם השיר</th>
|
||||||
<th>BPM</th>
|
<th>BPM</th>
|
||||||
<th>סולם</th>
|
<th>סולם</th>
|
||||||
<th>משך</th>
|
<th>משך</th>
|
||||||
<th>תגים</th>
|
<th>תגים</th>
|
||||||
|
<th>הערות</th>
|
||||||
<th>פעולות</th>
|
<th>פעולות</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody id="songsTableBody">
|
||||||
<?php if (empty($songs)): ?>
|
<?php if (empty($songs)): ?>
|
||||||
<tr>
|
<tr id="noSongsRow">
|
||||||
<td colspan="6" class="text-center text-muted py-4">עדיין אין שירים במאגר. <a href="#" id="addSongBtnLink">הוסף את השיר הראשון שלך!</a></td>
|
<td colspan="9" class="text-center text-muted py-4">
|
||||||
|
<?php if (!empty($search)): ?>
|
||||||
|
לא נמצאו שירים התואמים את החיפוש "<?php echo htmlspecialchars($search); ?>".
|
||||||
|
<?php else: ?>
|
||||||
|
עדיין אין שירים במאגר. <a href="#" id="addSongBtnLink">הוסף את השיר הראשון שלך!</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php foreach ($songs as $song): ?>
|
<?php foreach ($songs as $index => $song): ?>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td><?php echo $index + 1; ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($song['artist']); ?></td>
|
||||||
<td class="fw-bold"><?php echo htmlspecialchars($song['name']); ?></td>
|
<td class="fw-bold"><?php echo htmlspecialchars($song['name']); ?></td>
|
||||||
<td><?php echo htmlspecialchars($song['bpm']); ?></td>
|
<td><?php echo htmlspecialchars($song['bpm']); ?></td>
|
||||||
<td><?php echo htmlspecialchars($song['song_key']); ?></td>
|
<td><?php echo htmlspecialchars($song['song_key']); ?></td>
|
||||||
@ -131,6 +166,7 @@ include 'includes/header.php';
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</td>
|
</td>
|
||||||
|
<td><?php echo htmlspecialchars($song['notes']); ?></td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-primary edit-btn" data-song='<?php echo htmlspecialchars(json_encode($song), ENT_QUOTES, 'UTF-8'); ?>'>
|
<button class="btn btn-sm btn-outline-primary edit-btn" data-song='<?php echo htmlspecialchars(json_encode($song), ENT_QUOTES, 'UTF-8'); ?>'>
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
@ -164,9 +200,15 @@ include 'includes/header.php';
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="row">
|
||||||
<label for="name" class="form-label">שם השיר <span class="text-danger">*</span></label>
|
<div class="col-md-6 mb-3">
|
||||||
<input type="text" class="form-control" id="name" name="name" required>
|
<label for="artist" class="form-label">שם האמן</label>
|
||||||
|
<input type="text" class="form-control" id="artist" name="artist">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="name" class="form-label">שם השיר <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
@ -228,4 +270,5 @@ include 'includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="assets/js/songs_page.js?v=<?php echo time(); ?>"></script>
|
||||||
<?php include 'includes/footer.php'; ?>
|
<?php include 'includes/footer.php'; ?>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user