Auto commit: 2025-10-24T11:09:54.226Z

This commit is contained in:
Flatlogic Bot 2025-10-24 11:09:54 +00:00
parent 9a00d79244
commit 008dda1d41
27 changed files with 869 additions and 132 deletions

View 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
View 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);

View 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
View 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);

View 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('אירעה שגיאה קריטית בעת ההסרה.');
});
}
});
});

View File

@ -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 = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> יוצר...';
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();
}
});
});
});

178
assets/js/songs_page.js Normal file
View 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 {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;'
}[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();
}
});
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -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());
}

View 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;

View 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`);

View 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`;

View 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`);

View File

@ -0,0 +1 @@
ALTER TABLE `songs` ADD `artist` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL AFTER `name`;

View 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
View 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>';
}
?>

View File

@ -28,14 +28,15 @@
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<?php $pageName = basename($_SERVER['PHP_SELF'], '.php'); ?>
<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 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 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>
</ul>
</div>

View File

@ -1,4 +1,7 @@
<?php include 'includes/header.php'; ?>
<?php
$pageName = 'index';
include 'includes/header.php';
?>
<div class="text-center">
<h1 class="display-4 fw-bold">ברוכים הבאים ל-Ari Stage</h1>

95
lineup_details.php Normal file
View 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
View 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
View 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";
}
?>

View File

@ -1,4 +1,5 @@
<?php
$pageName = 'songs';
session_start();
require_once 'db/config.php';
@ -16,6 +17,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($name)) {
throw new Exception("שם השיר הוא שדה חובה.");
}
$artist = trim($_POST['artist'] ?? '');
$bpm = !empty($_POST['bpm']) ? (int)$_POST['bpm'] : null;
$key_note = $_POST['key_note'] ?? '';
@ -29,14 +31,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$duration_seconds = ($minutes * 60) + $seconds;
if ($action === 'create') {
$stmt = $pdo->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';
<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 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="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>#</th>
<th>שם האמן</th>
<th>שם השיר</th>
<th>BPM</th>
<th>סולם</th>
<th>משך</th>
<th>תגים</th>
<th>הערות</th>
<th>פעולות</th>
</tr>
</thead>
<tbody>
<tbody id="songsTableBody">
<?php if (empty($songs)): ?>
<tr>
<td colspan="6" class="text-center text-muted py-4">עדיין אין שירים במאגר. <a href="#" id="addSongBtnLink">הוסף את השיר הראשון שלך!</a></td>
<tr id="noSongsRow">
<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>
<?php else: ?>
<?php foreach ($songs as $song): ?>
<?php foreach ($songs as $index => $song): ?>
<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><?php echo htmlspecialchars($song['bpm']); ?></td>
<td><?php echo htmlspecialchars($song['song_key']); ?></td>
@ -131,6 +166,7 @@ include 'includes/header.php';
<?php endif; ?>
<?php endforeach; ?>
</td>
<td><?php echo htmlspecialchars($song['notes']); ?></td>
<td>
<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>
@ -164,9 +200,15 @@ include 'includes/header.php';
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="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 class="row">
<div class="col-md-6 mb-3">
<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 class="row">
<div class="col-md-4 mb-3">
@ -228,4 +270,5 @@ include 'includes/header.php';
</div>
</div>
<script src="assets/js/songs_page.js?v=<?php echo time(); ?>"></script>
<?php include 'includes/footer.php'; ?>