diff --git a/api/add_song_to_lineup.php b/api/add_song_to_lineup.php
new file mode 100644
index 0000000..dbc54d1
--- /dev/null
+++ b/api/add_song_to_lineup.php
@@ -0,0 +1,59 @@
+ 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()]);
+}
+?>
\ No newline at end of file
diff --git a/api/lineups_api.php b/api/lineups_api.php
new file mode 100644
index 0000000..9da844f
--- /dev/null
+++ b/api/lineups_api.php
@@ -0,0 +1,34 @@
+ 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);
diff --git a/api/remove_song_from_lineup.php b/api/remove_song_from_lineup.php
new file mode 100644
index 0000000..7d70d8f
--- /dev/null
+++ b/api/remove_song_from_lineup.php
@@ -0,0 +1,28 @@
+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.']);
+}
+?>
\ No newline at end of file
diff --git a/api/search_songs.php b/api/search_songs.php
new file mode 100644
index 0000000..d08ff87
--- /dev/null
+++ b/api/search_songs.php
@@ -0,0 +1,23 @@
+prepare($sql);
+$stmt->execute($params);
+$songs = $stmt->fetchAll();
+
+// --- Presentation ---
+header('Content-Type: application/json');
+echo json_encode($songs);
diff --git a/assets/js/lineup_details_page.js b/assets/js/lineup_details_page.js
new file mode 100644
index 0000000..0f6be58
--- /dev/null
+++ b/assets/js/lineup_details_page.js
@@ -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 = `
+ ${song.artist} - ${song.name}
+
+ `;
+ list.appendChild(listItem);
+ });
+ searchResultsContainer.appendChild(list);
+ } else {
+ searchResultsContainer.innerHTML = '
לא נמצאו שירים תואמים.
';
+ }
+ })
+ .catch(error => {
+ console.error('Error searching for songs:', error);
+ searchResultsContainer.innerHTML = 'אירעה שגיאה בחיפוש.
';
+ });
+ });
+
+ // 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('אירעה שגיאה קריטית בעת ההסרה.');
+ });
+ }
+ });
+});
diff --git a/assets/js/main.js b/assets/js/main.js
index c19d73a..3464641 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -1,81 +1,41 @@
document.addEventListener('DOMContentLoaded', function () {
- // Initialize toast notifications
- const toastEl = document.getElementById('notificationToast');
- if (toastEl) {
- const toast = new bootstrap.Toast(toastEl, { delay: 3000 });
- toast.show();
- }
+ // Handle Create Lineup form submission
+ const createLineupForm = document.getElementById('createLineupForm');
+ if (createLineupForm) {
+ createLineupForm.addEventListener('submit', function(event) {
+ 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');
- if (!songModalEl) return;
+ createButton.disabled = true;
+ createButton.innerHTML = ' יוצר...';
- 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 = 'הוספת שיר חדש';
- };
-
- // Handle clicks on edit buttons
- document.querySelectorAll('.edit-btn').forEach(button => {
- button.addEventListener('click', function () {
- const song = JSON.parse(this.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('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();
+ fetch('api/lineups_api.php', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ action: 'create', name: lineupName }),
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ window.location.reload(); // Reload to see the new lineup
+ } else {
+ alert('שגיאה ביצירת הליינאפ: ' + data.error);
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ alert('אירעה שגיאה בלתי צפויה.');
+ })
+ .finally(() => {
+ // This might not be reached if the page reloads, but it's good practice
+ createButton.disabled = false;
+ createButton.innerHTML = originalButtonText;
+ });
});
}
-
- // 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();
- }
- });
-});
\ No newline at end of file
+});
diff --git a/assets/js/songs_page.js b/assets/js/songs_page.js
new file mode 100644
index 0000000..0cc98d7
--- /dev/null
+++ b/assets/js/songs_page.js
@@ -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 = `לא נמצאו שירים התואמים את החיפוש "${searchTerm}". | `;
+ if (searchTerm === '') {
+ if(originalNoSongsRow){
+ tableBody.appendChild(originalNoSongsRow);
+ } else {
+ noResultsRow.innerHTML = `עדיין אין שירים במאגר. הוסף את השיר הראשון שלך! | `;
+ 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 ? `${escapeHTML(trimmedTag)}` : '';
+ }).join(' ');
+
+ row.innerHTML = `
+ ${index + 1} |
+ ${escapeHTML(song.artist)} |
+ ${escapeHTML(song.name)} |
+ ${escapeHTML(song.bpm)} |
+ ${escapeHTML(song.song_key)} |
+ ${formatDuration(song.duration_seconds)} |
+ ${tagsHtml} |
+ ${escapeHTML(song.notes)} |
+
+
+
+ |
+ `;
+ 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();
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/assets/pasted-20251024-093024-ed968ccb.png b/assets/pasted-20251024-093024-ed968ccb.png
new file mode 100644
index 0000000..6347707
Binary files /dev/null and b/assets/pasted-20251024-093024-ed968ccb.png differ
diff --git a/assets/pasted-20251024-093716-af880ec1.png b/assets/pasted-20251024-093716-af880ec1.png
new file mode 100644
index 0000000..5c05210
Binary files /dev/null and b/assets/pasted-20251024-093716-af880ec1.png differ
diff --git a/assets/pasted-20251024-094241-a54f51c9.png b/assets/pasted-20251024-094241-a54f51c9.png
new file mode 100644
index 0000000..7fd1854
Binary files /dev/null and b/assets/pasted-20251024-094241-a54f51c9.png differ
diff --git a/assets/pasted-20251024-100423-adeccb17.png b/assets/pasted-20251024-100423-adeccb17.png
new file mode 100644
index 0000000..3d20678
Binary files /dev/null and b/assets/pasted-20251024-100423-adeccb17.png differ
diff --git a/assets/pasted-20251024-101729-6227f5cd.png b/assets/pasted-20251024-101729-6227f5cd.png
new file mode 100644
index 0000000..a1e1eea
Binary files /dev/null and b/assets/pasted-20251024-101729-6227f5cd.png differ
diff --git a/assets/pasted-20251024-105800-5649a69d.png b/assets/pasted-20251024-105800-5649a69d.png
new file mode 100644
index 0000000..ef98b40
Binary files /dev/null and b/assets/pasted-20251024-105800-5649a69d.png differ
diff --git a/db/migrate.php b/db/migrate.php
deleted file mode 100644
index 7065604..0000000
--- a/db/migrate.php
+++ /dev/null
@@ -1,38 +0,0 @@
- 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());
-}
\ No newline at end of file
diff --git a/db/migrations/001_initial_schema.sql b/db/migrations/001_initial_schema.sql
new file mode 100644
index 0000000..a06e042
--- /dev/null
+++ b/db/migrations/001_initial_schema.sql
@@ -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;
+
diff --git a/db/migrations/002_add_indexes.sql b/db/migrations/002_add_indexes.sql
new file mode 100644
index 0000000..913a627
--- /dev/null
+++ b/db/migrations/002_add_indexes.sql
@@ -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`);
diff --git a/db/migrations/003_add_display_order_column.sql b/db/migrations/003_add_display_order_column.sql
new file mode 100644
index 0000000..0d2644d
--- /dev/null
+++ b/db/migrations/003_add_display_order_column.sql
@@ -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`;
diff --git a/db/migrations/004_add_order_index.sql b/db/migrations/004_add_order_index.sql
new file mode 100644
index 0000000..1d6f54f
--- /dev/null
+++ b/db/migrations/004_add_order_index.sql
@@ -0,0 +1,2 @@
+-- Add index for display_order to speed up sorting
+ALTER TABLE `lineup_songs` ADD INDEX `idx_display_order` (`display_order`);
diff --git a/db/migrations/005_add_artist_to_songs.sql b/db/migrations/005_add_artist_to_songs.sql
new file mode 100644
index 0000000..a5877b9
--- /dev/null
+++ b/db/migrations/005_add_artist_to_songs.sql
@@ -0,0 +1 @@
+ALTER TABLE `songs` ADD `artist` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL AFTER `name`;
diff --git a/db/migrations/006_add_song_order_index.sql b/db/migrations/006_add_song_order_index.sql
new file mode 100644
index 0000000..2e96789
--- /dev/null
+++ b/db/migrations/006_add_song_order_index.sql
@@ -0,0 +1,2 @@
+-- Add index for song_order to speed up sorting
+ALTER TABLE `lineup_songs` ADD INDEX `idx_song_order` (`song_order`);
diff --git a/debug_songs.php b/debug_songs.php
new file mode 100644
index 0000000..4318fe6
--- /dev/null
+++ b/debug_songs.php
@@ -0,0 +1,44 @@
+מצב מאגר השירים';
+echo 'בעמוד זה מוצגת רשימת השירים כפי שהיא שמורה כרגע במסד הנתונים.
';
+
+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 'המאגר ריק. לא נמצאו שירים.
';
+ } else {
+ echo '';
+ echo '| ID | Name | Artist | BPM | Key | Duration | Tags | Notes |
';
+ echo '';
+ foreach ($songs as $song) {
+ echo '';
+ echo '| ' . htmlspecialchars($song['id']) . ' | ';
+ echo '' . htmlspecialchars($song['name']) . ' | ';
+ echo '' . htmlspecialchars($song['artist'] ?? 'N/A') . ' | ';
+ echo '' . htmlspecialchars($song['bpm'] ?? 'N/A') . ' | ';
+ echo '' . htmlspecialchars($song['song_key'] ?? 'N/A') . ' | ';
+ echo '' . htmlspecialchars($song['duration_seconds'] ?? 'N/A') . ' | ';
+ echo '' . htmlspecialchars($song['tags'] ?? 'N/A') . ' | ';
+ echo '' . htmlspecialchars($song['notes'] ?? 'N/A') . ' | ';
+ echo '
';
+ }
+ echo '
';
+ echo 'סה"כ שירים: ' . count($songs) . '
';
+ }
+
+} catch (Exception $e) {
+ echo '';
+ echo 'שגיאה חמורה!
';
+ echo 'לא ניתן היה להתחבר למסד הנתונים או לשלוף את המידע.
';
+ echo 'פרטי השגיאה: ' . $e->getMessage();
+ echo '
';
+}
+?>
diff --git a/includes/header.php b/includes/header.php
index 78fbcf0..e2bb676 100644
--- a/includes/header.php
+++ b/includes/header.php
@@ -28,14 +28,15 @@
diff --git a/index.php b/index.php
index a54858a..a2631ed 100644
--- a/index.php
+++ b/index.php
@@ -1,4 +1,7 @@
-
+
ברוכים הבאים ל-Ari Stage
diff --git a/lineup_details.php b/lineup_details.php
new file mode 100644
index 0000000..f2b8a04
--- /dev/null
+++ b/lineup_details.php
@@ -0,0 +1,95 @@
+
מזהה ליינאפ לא תקין.
';
+ 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 '';
+ 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);
+
+?>
+
+
+
+
+
+
+
+
+
+
+
+
שירים בליינאפ
+
+
+
+ -
+ אין עדיין שירים בליינאפ זה.
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lineups.php b/lineups.php
new file mode 100644
index 0000000..53b04a6
--- /dev/null
+++ b/lineups.php
@@ -0,0 +1,72 @@
+
+
+
+
+
הליינאפים שלי
+
+
+
+ query("SELECT * FROM lineups ORDER BY created_at DESC");
+ $lineups = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ } catch (PDOException $e) {
+ // Handle DB error
+ $lineups = [];
+ echo "
שגיאה בטעינת הליינאפים: " . htmlspecialchars($e->getMessage()) . "
";
+ }
+ ?>
+
+
+
+
+
עדיין לא יצרת ליינאפים.
+
לחץ על "צור ליינאפ חדש" כדי להתחיל.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/migrate.php b/migrate.php
new file mode 100644
index 0000000..f57eecb
--- /dev/null
+++ b/migrate.php
@@ -0,0 +1,77 @@
+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";
+}
+
+?>
\ No newline at end of file
diff --git a/songs.php b/songs.php
index fe62850..80a4cb6 100644
--- a/songs.php
+++ b/songs.php
@@ -1,4 +1,5 @@
prepare("INSERT INTO songs (name, bpm, song_key, duration_seconds, notes, tags) VALUES (?, ?, ?, ?, ?, ?)");
- $stmt->execute([$name, $bpm, $song_key, $duration_seconds, $notes, $tags]);
+ $stmt = $pdo->prepare("INSERT INTO songs (name, artist, bpm, song_key, duration_seconds, notes, tags) VALUES (?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$name, $artist, $bpm, $song_key, $duration_seconds, $notes, $tags]);
$_SESSION['notification'] = ['message' => 'השיר נוצר בהצלחה!', 'type' => 'success'];
} else { // update
$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
- $stmt = $pdo->prepare("UPDATE songs SET name=?, bpm=?, song_key=?, duration_seconds=?, notes=?, tags=? WHERE id=?");
- $stmt->execute([$name, $bpm, $song_key, $duration_seconds, $notes, $tags, $id]);
+ $stmt = $pdo->prepare("UPDATE songs SET name=?, artist=?, bpm=?, song_key=?, duration_seconds=?, notes=?, tags=? WHERE id=?");
+ $stmt->execute([$name, $artist, $bpm, $song_key, $duration_seconds, $notes, $tags, $id]);
$_SESSION['notification'] = ['message' => 'השיר עודכן בהצלחה!', 'type' => 'success'];
}
}
@@ -64,8 +66,19 @@ if (isset($_SESSION['notification'])) {
}
-// Fetch all songs to display
-$songs = $pdo->query("SELECT * FROM songs ORDER BY name ASC")->fetchAll();
+// Fetch songs with search functionality
+$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) {
if ($seconds === null || $seconds < 0) return '00:00';
@@ -98,28 +111,50 @@ include 'includes/header.php';
-
+
+
+
+
+ | # |
+ שם האמן |
שם השיר |
BPM |
סולם |
משך |
תגים |
+ הערות |
פעולות |
-
+
-
- | עדיין אין שירים במאגר. הוסף את השיר הראשון שלך! |
+
+ |
+
+ לא נמצאו שירים התואמים את החיפוש "".
+
+ עדיין אין שירים במאגר. הוסף את השיר הראשון שלך!
+
+ |
-
+ $song): ?>
+ |
+ |
|
|
|
@@ -131,6 +166,7 @@ include 'includes/header.php';
+ |
|