Compare commits

...

3 Commits

Author SHA1 Message Date
Flatlogic Bot
b4250586c9 עובד 2025-10-24 11:33:18 +00:00
Flatlogic Bot
008dda1d41 Auto commit: 2025-10-24T11:09:54.226Z 2025-10-24 11:09:54 +00:00
Flatlogic Bot
9a00d79244 גרסה 1 2025-10-24 08:37:00 +00:00
28 changed files with 1240 additions and 148 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()]);
}
?>

57
api/lineups_api.php Normal file
View File

@ -0,0 +1,57 @@
<?php
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
$response = ['success' => false, 'error' => 'Invalid request'];
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if (isset($_GET['lineup_id']) && is_numeric($_GET['lineup_id'])) {
try {
$lineup_id = intval($_GET['lineup_id']);
$pdo = db();
$stmt = $pdo->prepare("
SELECT s.*, ls.song_order FROM songs s
JOIN lineup_songs ls ON s.id = ls.song_id
WHERE ls.lineup_id = ?
ORDER BY ls.song_order ASC
");
$stmt->execute([$lineup_id]);
$songs = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($songs);
exit;
} catch (PDOException $e) {
$response['error'] = 'Database error: ' . $e->getMessage();
}
} else {
$response['error'] = 'Lineup ID is required.';
}
}
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);

53
assets/css/custom.css Normal file
View File

@ -0,0 +1,53 @@
body {
font-family: 'Heebo', sans-serif;
background-color: #F8FAFC;
}
.navbar-brand {
font-weight: 700;
font-size: 1.5rem;
}
.btn-primary {
background-color: #1D4ED8;
border-color: #1D4ED8;
}
.btn-primary:hover {
background-color: #1E40AF;
border-color: #1E40AF;
}
.btn-secondary {
background-color: #F97316;
border-color: #F97316;
}
.btn-secondary:hover {
background-color: #EA580C;
border-color: #EA580C;
}
.card {
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.05), 0 1px 2px -1px rgb(0 0 0 / 0.05);
}
.form-control, .form-select {
border-radius: 0.5rem;
}
.form-control:focus, .form-select:focus {
border-color: #1D4ED8;
box-shadow: 0 0 0 0.25rem rgba(29, 78, 216, 0.25);
}
.table {
vertical-align: middle;
}
.duration-input-group {
direction: ltr;
}
.toast-container {
z-index: 1080;
}

View File

@ -0,0 +1,130 @@
document.addEventListener('DOMContentLoaded', function () {
const lineupId = new URLSearchParams(window.location.search).get('id');
const songSearchInput = document.getElementById('song-search-input');
const searchResultsContainer = document.getElementById('search-results');
const lineupSongsContainer = document.getElementById('lineup-song-list');
// Function to fetch and display songs already in the lineup
function fetchLineupSongs() {
if (!lineupId) return;
fetch(`/api/lineups_api.php?lineup_id=${lineupId}`)
.then(response => response.json())
.then(data => {
renderLineupSongs(data);
})
.catch(error => console.error('Error fetching lineup songs:', error));
}
// Function to render the list of songs in the lineup
function renderLineupSongs(songs) {
lineupSongsContainer.innerHTML = '';
if (songs.length === 0) {
lineupSongsContainer.innerHTML = '<p>אין עדיין שירים בליינאפ זה.</p>';
return;
}
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.textContent = `${song.artist || 'Unknown Artist'} - ${song.name}`;
const removeBtn = document.createElement('button');
removeBtn.className = 'btn btn-danger btn-sm';
removeBtn.textContent = 'הסר';
removeBtn.onclick = () => removeSongFromLineup(song.id); // Use song.id from the songs table
listItem.appendChild(removeBtn);
list.appendChild(listItem);
});
lineupSongsContainer.appendChild(list);
}
// Function to search for songs
function searchSongs(query) {
fetch(`/api/search_songs.php?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
renderSearchResults(data);
})
.catch(error => console.error('Error searching songs:', error));
}
// Function to render search results
function renderSearchResults(songs) {
searchResultsContainer.innerHTML = '';
if (songs.length === 0) {
searchResultsContainer.innerHTML = '<p>לא נמצאו שירים.</p>';
return;
}
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.textContent = `${song.artist || 'Unknown Artist'} - ${song.name}`;
const addBtn = document.createElement('button');
addBtn.className = 'btn btn-primary btn-sm';
addBtn.textContent = 'הוסף';
addBtn.onclick = () => addSongToLineup(song.id);
listItem.appendChild(addBtn);
list.appendChild(listItem);
});
searchResultsContainer.appendChild(list);
}
// Function to add a song to the lineup
function addSongToLineup(songId) {
fetch('/api/add_song_to_lineup.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ lineup_id: lineupId, song_id: songId })
})
.then(response => response.json().then(data => ({ status: response.status, body: data })))
.then(({ status, body }) => {
if (status === 200 && body.success) {
fetchLineupSongs(); // Refresh the lineup list
const currentQuery = searchInput.value.trim();
searchSongs(currentQuery); // Refresh search results to remove the added song
} else {
// Use the specific message from the server, or a default one
alert(body.message || 'לא ניתן היה להוסיף את השיר.');
}
})
.catch(error => console.error('Error adding song:', error));
}
// Function to remove a song from the lineup
function removeSongFromLineup(songId) {
fetch('/api/remove_song_from_lineup.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ lineup_id: lineupId, song_id: songId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
fetchLineupSongs(); // Refresh the lineup list
const currentQuery = searchInput.value.trim();
searchSongs(currentQuery); // Refresh search results to show the removed song
} else {
alert('Failed to remove song: ' + (data.error || 'Unknown error'));
}
})
.catch(error => console.error('Error removing song:', error));
}
// Initial fetch of lineup songs and all songs for searching
if (lineupId) {
fetchLineupSongs();
searchSongs(''); // Load all songs initially
}
// Event Listener for the search input
searchInput.addEventListener('input', () => {
const query = searchInput.value.trim();
searchSongs(query);
});
});

41
assets/js/main.js Normal file
View File

@ -0,0 +1,41 @@
document.addEventListener('DOMContentLoaded', function () {
// 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;
createButton.disabled = true;
createButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> יוצר...';
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;
});
});
}
});

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

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

9
includes/footer.php Normal file
View File

@ -0,0 +1,9 @@
</main>
<footer class="text-center py-4 text-muted border-top mt-5">
<p>&copy; <?php echo date('Y'); ?> Ari Stage. Built with Flatlogic.</p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</body>
</html>

45
includes/header.php Normal file
View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ari Stage - ניהול מופעים</title>
<meta name="description" content="מערכת ליינאפ וניהול מופעים לזמרים ומפיקים. נבנה עם Flatlogic.">
<meta name="keywords" content="ניהול הופעות, ליינאפ, רשימת שירים, ניהול שירים, מוזיקה, זמרים, מפיקים, Ari Stage, Flatlogic">
<meta property="og:title" content="Ari Stage">
<meta property="og:description" content="מערכת ליינאפ וניהול מופעים לזמרים ומפיקים.">
<meta property="og:image" content="">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #1D4ED8;">
<div class="container-fluid">
<a class="navbar-brand fw-bold" href="index.php">Ari Stage</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</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 <?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 <?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 <?php echo ($pageName == 'lineups') ? 'active' : ''; ?>" <?php echo ($pageName == 'lineups') ? 'aria-current="page"' : ''; ?> href="lineups.php">ליינאפים</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container my-5">

161
index.php
View File

@ -1,150 +1,17 @@
<?php <?php
declare(strict_types=1); $pageName = 'index';
@ini_set('display_errors', '1'); include 'includes/header.php';
@error_reporting(E_ALL); ?>
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; <div class="text-center">
$now = date('Y-m-d H:i:s'); <h1 class="display-4 fw-bold">ברוכים הבאים ל-Ari Stage</h1>
?> <p class="lead col-lg-6 mx-auto text-muted">
<!doctype html> הכלי המושלם לזמרים ומפיקים לניהול מאגר השירים, יצירת ליינאפים דינמיים, וארגון הופעות.
<html lang="en"> </p>
<head> <div class="d-grid gap-2 d-sm-flex justify-content-sm-center mt-5">
<meta charset="utf-8" /> <a href="songs.php" class="btn btn-primary btn-lg px-4 gap-3">למאגר השירים שלי</a>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <a href="#" class="btn btn-outline-secondary btn-lg px-4 disabled">צור ליינאפ חדש</a>
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</main> </div>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <?php include 'includes/footer.php'; ?>
</footer>
</body>
</html>

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="search-song-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";
}
?>

274
songs.php Normal file
View File

@ -0,0 +1,274 @@
<?php
$pageName = 'songs';
session_start();
require_once 'db/config.php';
// --- Logic ---
$pdo = db();
$notification = null;
// Handle POST requests for CUD operations
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
try {
if ($action === 'create' || $action === 'update') {
$name = trim($_POST['name'] ?? '');
if (empty($name)) {
throw new Exception("שם השיר הוא שדה חובה.");
}
$artist = trim($_POST['artist'] ?? '');
$bpm = !empty($_POST['bpm']) ? (int)$_POST['bpm'] : null;
$key_note = $_POST['key_note'] ?? '';
$key_scale = $_POST['key_scale'] ?? '';
$song_key = trim($key_note . ' ' . $key_scale);
$notes = trim($_POST['notes'] ?? '');
$tags = trim($_POST['tags'] ?? '');
$minutes = !empty($_POST['duration_minutes']) ? (int)$_POST['duration_minutes'] : 0;
$seconds = !empty($_POST['duration_seconds']) ? (int)$_POST['duration_seconds'] : 0;
$duration_seconds = ($minutes * 60) + $seconds;
if ($action === 'create') {
$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=?, 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'];
}
}
} elseif ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
$stmt = $pdo->prepare("DELETE FROM songs WHERE id=?");
$stmt->execute([$id]);
$_SESSION['notification'] = ['message' => 'השיר נמחק בהצלחה.', 'type' => 'danger'];
}
}
} catch (Exception $e) {
$_SESSION['notification'] = ['message' => 'אירעה שגיאה: ' . $e->getMessage(), 'type' => 'danger'];
}
// Redirect to avoid form resubmission
header("Location: songs.php");
exit();
}
// Check for notification from session
if (isset($_SESSION['notification'])) {
$notification = $_SESSION['notification'];
unset($_SESSION['notification']);
}
// 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';
$mins = floor($seconds / 60);
$secs = $seconds % 60;
return sprintf('%02d:%02d', $mins, $secs);
}
// --- Presentation ---
include 'includes/header.php';
?>
<!-- Notification Toast -->
<?php if ($notification): ?>
<div class="toast-container position-fixed bottom-0 start-50 translate-middle-x p-3">
<div id="notificationToast" class="toast align-items-center text-bg-<?php echo htmlspecialchars($notification['type']); ?> border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<?php echo htmlspecialchars($notification['message']); ?>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
<?php endif; ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">מאגר השירים</h1>
<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>
<!-- 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 id="songsTableBody">
<?php if (empty($songs)): ?>
<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 $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>
<td><?php echo format_duration($song['duration_seconds']); ?></td>
<td>
<?php foreach(explode(',', $song['tags']) as $tag): ?>
<?php if(trim($tag)): ?>
<span class="badge bg-secondary bg-opacity-25 text-dark-emphasis"><?php echo htmlspecialchars(trim($tag)); ?></span>
<?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>
</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="<?php echo $song['id']; ?>">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Add/Edit Song Modal -->
<div class="modal fade" id="songModal" tabindex="-1" aria-labelledby="songModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form id="songForm" action="songs.php" method="POST">
<input type="hidden" name="action" id="action" value="create">
<input type="hidden" name="id" id="song_id">
<div class="modal-header">
<h5 class="modal-title" id="songModalLabel">הוספת שיר חדש</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<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">
<label for="bpm" class="form-label">BPM</label>
<input type="number" class="form-control" id="bpm" name="bpm">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">סולם</label>
<div class="input-group">
<select class="form-select" id="key_note" name="key_note">
<option value="" selected disabled>תו</option>
<option value="C">C</option>
<option value="C#">C#</option>
<option value="D">D</option>
<option value="D#">D#</option>
<option value="E">E</option>
<option value="F">F</option>
<option value="F#">F#</option>
<option value="G">G</option>
<option value="G#">G#</option>
<option value="A">A</option>
<option value="A#">A#</option>
<option value="B">B</option>
</select>
<select class="form-select" id="key_scale" name="key_scale">
<option value="Major">Major</option>
<option value="Minor">Minor</option>
</select>
</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">משך (mm:ss)</label>
<div class="input-group duration-input-group">
<input type="number" class="form-control text-center" id="duration_minutes" name="duration_minutes" placeholder="00" min="0" max="59">
<span class="input-group-text">:</span>
<input type="number" class="form-control text-center" id="duration_seconds" name="duration_seconds" placeholder="00" min="0" max="59">
</div>
</div>
</div>
<div class="mb-3">
<label for="tags" class="form-label">תגים / סגנון</label>
<input type="text" class="form-control" id="tags" name="tags" placeholder="מופרד בפסיקים, לדוגמה: רוק, קאבר, שקט">
</div>
<div class="mb-3">
<label for="notes" class="form-label">הערות</label>
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">ביטול</button>
<button type="submit" class="btn btn-primary">שמור שיר</button>
</div>
</form>
<form id="deleteForm" action="songs.php" method="POST" class="d-none">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id">
</form>
</div>
</div>
</div>
<script src="assets/js/songs_page.js?v=<?php echo time(); ?>"></script>
<?php include 'includes/footer.php'; ?>