Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4250586c9 | ||
|
|
008dda1d41 | ||
|
|
9a00d79244 |
59
api/add_song_to_lineup.php
Normal file
59
api/add_song_to_lineup.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
// Get the posted data
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!$data || !isset($data['lineup_id']) || !isset($data['song_id'])) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid input.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lineup_id = $data['lineup_id'];
|
||||||
|
$song_id = $data['song_id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Check if the song is already in the lineup
|
||||||
|
$stmt = $pdo->prepare("SELECT 1 FROM lineup_songs WHERE lineup_id = ? AND song_id = ?");
|
||||||
|
$stmt->execute([$lineup_id, $song_id]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
http_response_code(409); // 409 Conflict
|
||||||
|
echo json_encode(['success' => false, 'message' => 'השיר כבר קיים בליינאפ.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current number of songs in the lineup to determine the new order.
|
||||||
|
// This is much faster than calculating MAX(song_order).
|
||||||
|
$stmt = $pdo->prepare("SELECT COUNT(*) as song_count FROM lineup_songs WHERE lineup_id = ?");
|
||||||
|
$stmt->execute([$lineup_id]);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$new_order = $result ? (int)$result['song_count'] : 0;
|
||||||
|
|
||||||
|
|
||||||
|
// Insert the new song
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO lineup_songs (lineup_id, song_id, song_order) VALUES (?, ?, ?)");
|
||||||
|
|
||||||
|
if ($stmt->execute([$lineup_id, $song_id, $new_order])) {
|
||||||
|
// Fetch the added song details to return to the client
|
||||||
|
$song_stmt = $pdo->prepare("SELECT * FROM songs WHERE id = ?");
|
||||||
|
$song_stmt->execute([$song_id]);
|
||||||
|
$song = $song_stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'השיר נוסף בהצלחה!', 'song' => $song]);
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'לא ניתן היה להוסיף את השיר.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Log error to a file for debugging
|
||||||
|
error_log("Add song to lineup failed: " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
// Return a more specific error message for debugging
|
||||||
|
echo json_encode(['success' => false, 'message' => 'שגיאת שרת: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
57
api/lineups_api.php
Normal file
57
api/lineups_api.php
Normal 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);
|
||||||
28
api/remove_song_from_lineup.php
Normal file
28
api/remove_song_from_lineup.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
require_once '../db/config.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$lineup_id = $data['lineup_id'] ?? null;
|
||||||
|
$song_id = $data['song_id'] ?? null;
|
||||||
|
|
||||||
|
if ($lineup_id && $song_id) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM lineup_songs WHERE lineup_id = ? AND song_id = ?");
|
||||||
|
$stmt->execute([$lineup_id, $song_id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid input.']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Method not allowed.']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
23
api/search_songs.php
Normal file
23
api/search_songs.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
require_once '../db/config.php';
|
||||||
|
|
||||||
|
// --- Logic ---
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Fetch songs with search functionality
|
||||||
|
$search = $_GET['q'] ?? $_GET['search'] ?? '';
|
||||||
|
$sql = "SELECT * FROM songs";
|
||||||
|
$params = [];
|
||||||
|
if (!empty($search)) {
|
||||||
|
$sql .= " WHERE name LIKE ? OR artist LIKE ? OR bpm LIKE ? OR song_key LIKE ? OR notes LIKE ? OR tags LIKE ?";
|
||||||
|
$searchTerm = "%{$search}%";
|
||||||
|
$params = [$searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm, $searchTerm];
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY id ASC";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$songs = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// --- Presentation ---
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($songs);
|
||||||
53
assets/css/custom.css
Normal file
53
assets/css/custom.css
Normal 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;
|
||||||
|
}
|
||||||
130
assets/js/lineup_details_page.js
Normal file
130
assets/js/lineup_details_page.js
Normal 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
41
assets/js/main.js
Normal 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
178
assets/js/songs_page.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Live search for songs page
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const clearSearchBtn = document.getElementById('clearSearchBtn');
|
||||||
|
const tableBody = document.getElementById('songsTableBody');
|
||||||
|
const originalNoSongsRow = document.getElementById('noSongsRow')?.cloneNode(true);
|
||||||
|
|
||||||
|
if (searchInput && tableBody) {
|
||||||
|
// Function to format duration from seconds to MM:SS
|
||||||
|
const formatDuration = (seconds) => {
|
||||||
|
if (seconds === null || seconds < 0) return '00:00';
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to render table rows
|
||||||
|
const renderTable = (songs, searchTerm) => {
|
||||||
|
tableBody.innerHTML = ''; // Clear existing rows
|
||||||
|
|
||||||
|
if (songs.length === 0) {
|
||||||
|
const noResultsRow = document.createElement('tr');
|
||||||
|
noResultsRow.innerHTML = `<td colspan="9" class="text-center text-muted py-4">לא נמצאו שירים התואמים את החיפוש "${searchTerm}".</td>`;
|
||||||
|
if (searchTerm === '') {
|
||||||
|
if(originalNoSongsRow){
|
||||||
|
tableBody.appendChild(originalNoSongsRow);
|
||||||
|
} else {
|
||||||
|
noResultsRow.innerHTML = `<td colspan="9" class="text-center text-muted py-4">עדיין אין שירים במאגר. <a href="#" onclick="document.getElementById('addSongBtn').click()">הוסף את השיר הראשון שלך!</a></td>`;
|
||||||
|
tableBody.appendChild(noResultsRow);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tableBody.appendChild(noResultsRow);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
songs.forEach((song, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const tagsHtml = song.tags.split(',').map(tag => {
|
||||||
|
const trimmedTag = tag.trim();
|
||||||
|
return trimmedTag ? `<span class="badge bg-secondary bg-opacity-25 text-dark-emphasis">${escapeHTML(trimmedTag)}</span>` : '';
|
||||||
|
}).join(' ');
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td>${escapeHTML(song.artist)}</td>
|
||||||
|
<td class="fw-bold">${escapeHTML(song.name)}</td>
|
||||||
|
<td>${escapeHTML(song.bpm)}</td>
|
||||||
|
<td>${escapeHTML(song.song_key)}</td>
|
||||||
|
<td>${formatDuration(song.duration_seconds)}</td>
|
||||||
|
<td>${tagsHtml}</td>
|
||||||
|
<td>${escapeHTML(song.notes)}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary edit-btn" data-song='${escapeHTML(JSON.stringify(song))}' data-bs-toggle="modal" data-bs-target="#songModal">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
<form action="songs.php" method="POST" class="d-inline" onsubmit="return confirm('האם אתה בטוח שברצונך למחוק את השיר?');">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="id" value="${song.id}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debounce function to limit API calls
|
||||||
|
let debounceTimeout;
|
||||||
|
const handleSearch = () => {
|
||||||
|
const searchTerm = searchInput.value.trim();
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
|
||||||
|
clearSearchBtn.style.display = searchTerm ? 'block' : 'none';
|
||||||
|
|
||||||
|
debounceTimeout = setTimeout(() => {
|
||||||
|
fetch(`api/search_songs.php?search=${encodeURIComponent(searchTerm)}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(songs => {
|
||||||
|
renderTable(songs, searchTerm);
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching search results:', error));
|
||||||
|
}, 300); // 300ms delay
|
||||||
|
};
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', handleSearch);
|
||||||
|
|
||||||
|
clearSearchBtn.addEventListener('click', () => {
|
||||||
|
searchInput.value = '';
|
||||||
|
handleSearch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHTML(str) {
|
||||||
|
if (str === null || str === undefined) return '';
|
||||||
|
return String(str).replace(/[&<>'"/]/g, function (s) {
|
||||||
|
return {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
'/': '/'
|
||||||
|
}[s];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const songModalEl = document.getElementById('songModal');
|
||||||
|
if (songModalEl) {
|
||||||
|
const songModal = new bootstrap.Modal(songModalEl);
|
||||||
|
const modalTitle = songModalEl.querySelector('.modal-title');
|
||||||
|
const songForm = document.getElementById('songForm');
|
||||||
|
const actionInput = document.getElementById('action');
|
||||||
|
const songIdInput = document.getElementById('song_id');
|
||||||
|
|
||||||
|
// Function to reset the modal to its "Add Song" state
|
||||||
|
const resetModal = () => {
|
||||||
|
songForm.reset();
|
||||||
|
actionInput.value = 'create';
|
||||||
|
songIdInput.value = '';
|
||||||
|
modalTitle.textContent = 'הוספת שיר חדש';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use event delegation for edit buttons since they are dynamically loaded
|
||||||
|
document.body.addEventListener('click', function(event) {
|
||||||
|
const editBtn = event.target.closest('.edit-btn');
|
||||||
|
if (editBtn) {
|
||||||
|
const song = JSON.parse(editBtn.dataset.song);
|
||||||
|
|
||||||
|
resetModal(); // Start with a clean slate
|
||||||
|
|
||||||
|
actionInput.value = 'update';
|
||||||
|
modalTitle.textContent = 'עריכת שיר';
|
||||||
|
songIdInput.value = song.id;
|
||||||
|
|
||||||
|
document.getElementById('name').value = song.name;
|
||||||
|
document.getElementById('artist').value = song.artist;
|
||||||
|
document.getElementById('bpm').value = song.bpm;
|
||||||
|
|
||||||
|
if (song.song_key && song.song_key.trim() !== '') {
|
||||||
|
const keyParts = song.song_key.split(' ');
|
||||||
|
document.getElementById('key_note').value = keyParts[0];
|
||||||
|
document.getElementById('key_scale').value = keyParts[1] || 'Major';
|
||||||
|
} else {
|
||||||
|
document.getElementById('key_note').value = '';
|
||||||
|
document.getElementById('key_scale').value = 'Major';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('notes').value = song.notes;
|
||||||
|
document.getElementById('tags').value = song.tags;
|
||||||
|
|
||||||
|
if (song.duration_seconds) {
|
||||||
|
const minutes = Math.floor(song.duration_seconds / 60);
|
||||||
|
const seconds = song.duration_seconds % 60;
|
||||||
|
document.getElementById('duration_minutes').value = minutes;
|
||||||
|
document.getElementById('duration_seconds').value = seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
songModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSongLink = event.target.closest('#addSongBtnLink');
|
||||||
|
if(addSongLink){
|
||||||
|
event.preventDefault();
|
||||||
|
resetModal();
|
||||||
|
songModal.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset the modal form when it's opened via the main "Add Song" button
|
||||||
|
songModalEl.addEventListener('show.bs.modal', function (event) {
|
||||||
|
if (event.relatedTarget && !event.relatedTarget.classList.contains('edit-btn')) {
|
||||||
|
resetModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
BIN
assets/pasted-20251024-093024-ed968ccb.png
Normal file
BIN
assets/pasted-20251024-093024-ed968ccb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/pasted-20251024-093716-af880ec1.png
Normal file
BIN
assets/pasted-20251024-093716-af880ec1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
assets/pasted-20251024-094241-a54f51c9.png
Normal file
BIN
assets/pasted-20251024-094241-a54f51c9.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/pasted-20251024-100423-adeccb17.png
Normal file
BIN
assets/pasted-20251024-100423-adeccb17.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/pasted-20251024-101729-6227f5cd.png
Normal file
BIN
assets/pasted-20251024-101729-6227f5cd.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/pasted-20251024-105800-5649a69d.png
Normal file
BIN
assets/pasted-20251024-105800-5649a69d.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
26
db/migrations/001_initial_schema.sql
Normal file
26
db/migrations/001_initial_schema.sql
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
-- Initial Schema for Lineup Application
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `lineups` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`name` VARCHAR(255) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `songs` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`title` VARCHAR(255) NOT NULL,
|
||||||
|
`artist` VARCHAR(255) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `lineup_songs` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`lineup_id` INT NOT NULL,
|
||||||
|
`song_id` INT NOT NULL,
|
||||||
|
`display_order` INT NOT NULL DEFAULT 0,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`lineup_id`) REFERENCES `lineups`(`id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`song_id`) REFERENCES `songs`(`id`) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY `lineup_song_unique` (`lineup_id`, `song_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
7
db/migrations/002_add_indexes.sql
Normal file
7
db/migrations/002_add_indexes.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-- Add indexes to lineup_songs for faster lookups
|
||||||
|
ALTER TABLE `lineup_songs` ADD INDEX `idx_lineup_id` (`lineup_id`);
|
||||||
|
ALTER TABLE `lineup_songs` ADD INDEX `idx_song_id` (`song_id`);
|
||||||
|
|
||||||
|
-- Add a unique constraint to prevent adding the same song to a lineup twice
|
||||||
|
-- This also creates an index
|
||||||
|
ALTER TABLE `lineup_songs` ADD UNIQUE `unique_lineup_song` (`lineup_id`, `song_id`);
|
||||||
2
db/migrations/003_add_display_order_column.sql
Normal file
2
db/migrations/003_add_display_order_column.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add display_order column to lineup_songs table
|
||||||
|
ALTER TABLE `lineup_songs` ADD COLUMN `display_order` INT NOT NULL DEFAULT 0 AFTER `song_id`;
|
||||||
2
db/migrations/004_add_order_index.sql
Normal file
2
db/migrations/004_add_order_index.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add index for display_order to speed up sorting
|
||||||
|
ALTER TABLE `lineup_songs` ADD INDEX `idx_display_order` (`display_order`);
|
||||||
1
db/migrations/005_add_artist_to_songs.sql
Normal file
1
db/migrations/005_add_artist_to_songs.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `songs` ADD `artist` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL AFTER `name`;
|
||||||
2
db/migrations/006_add_song_order_index.sql
Normal file
2
db/migrations/006_add_song_order_index.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add index for song_order to speed up sorting
|
||||||
|
ALTER TABLE `lineup_songs` ADD INDEX `idx_song_order` (`song_order`);
|
||||||
44
debug_songs.php
Normal file
44
debug_songs.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
echo '<h1>מצב מאגר השירים</h1>';
|
||||||
|
echo '<p>בעמוד זה מוצגת רשימת השירים כפי שהיא שמורה כרגע במסד הנתונים.</p>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$stmt = $pdo->query("SELECT * FROM songs ORDER BY id ASC");
|
||||||
|
$songs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($songs)) {
|
||||||
|
echo '<div style="border: 1px solid #ccc; padding: 15px; background-color: #f0f0f0;"><strong>המאגר ריק.</strong> לא נמצאו שירים.</div>';
|
||||||
|
} else {
|
||||||
|
echo '<table border="1" cellpadding="5" cellspacing="0" style="width: 100%; border-collapse: collapse;">';
|
||||||
|
echo '<thead style="background-color: #f2f2f2;"><tr><th>ID</th><th>Name</th><th>Artist</th><th>BPM</th><th>Key</th><th>Duration</th><th>Tags</th><th>Notes</th></tr></thead>';
|
||||||
|
echo '<tbody>';
|
||||||
|
foreach ($songs as $song) {
|
||||||
|
echo '<tr>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['id']) . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['name']) . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['artist'] ?? 'N/A') . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['bpm'] ?? 'N/A') . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['song_key'] ?? 'N/A') . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['duration_seconds'] ?? 'N/A') . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['tags'] ?? 'N/A') . '</td>';
|
||||||
|
echo '<td>' . htmlspecialchars($song['notes'] ?? 'N/A') . '</td>';
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
echo '</tbody></table>';
|
||||||
|
echo '<p style="margin-top: 15px;"><strong>סה"כ שירים: ' . count($songs) . '</strong></p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo '<div style="border: 1px solid red; padding: 15px; background-color: #ffebeb; color: red;">';
|
||||||
|
echo '<strong>שגיאה חמורה!</strong><br>';
|
||||||
|
echo 'לא ניתן היה להתחבר למסד הנתונים או לשלוף את המידע.<br>';
|
||||||
|
echo 'פרטי השגיאה: ' . $e->getMessage();
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
9
includes/footer.php
Normal file
9
includes/footer.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
</main>
|
||||||
|
<footer class="text-center py-4 text-muted border-top mt-5">
|
||||||
|
<p>© <?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
45
includes/header.php
Normal 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
161
index.php
@ -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>
|
</div>
|
||||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
</div>
|
||||||
<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>
|
<?php include 'includes/footer.php'; ?>
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
95
lineup_details.php
Normal file
95
lineup_details.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
$pageName = 'פרטי ליינאפ';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
// Check if lineup ID is provided
|
||||||
|
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||||
|
echo '<div class="container mt-4"><div class="alert alert-danger">מזהה ליינאפ לא תקין.</div></div>';
|
||||||
|
require_once 'includes/footer.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lineup_id = intval($_GET['id']);
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
// Fetch lineup details
|
||||||
|
$stmt = $db->prepare("SELECT * FROM lineups WHERE id = ?");
|
||||||
|
$stmt->execute([$lineup_id]);
|
||||||
|
$lineup = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$lineup) {
|
||||||
|
echo '<div class="container mt-4"><div class="alert alert-danger">הליינאפ לא נמצא.</div></div>';
|
||||||
|
require_once 'includes/footer.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch songs for this lineup (to be implemented)
|
||||||
|
$songs_stmt = $db->prepare("
|
||||||
|
SELECT s.* FROM songs s
|
||||||
|
JOIN lineup_songs ls ON s.id = ls.song_id
|
||||||
|
WHERE ls.lineup_id = ?
|
||||||
|
ORDER BY ls.song_order ASC
|
||||||
|
");
|
||||||
|
$songs_stmt->execute([$lineup_id]);
|
||||||
|
$songs = $songs_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1><?php echo htmlspecialchars($lineup['name']); ?></h1>
|
||||||
|
<a href="lineups.php" class="btn btn-outline-secondary">חזרה לרשימת הליינאפים</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Add Songs -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h3>הוספת שירים</h3>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="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
72
lineups.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
$pageTitle = 'הליינאפים שלי';
|
||||||
|
$pageName = 'lineups';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
require_once __DIR__ . '/includes/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>הליינאפים שלי</h1>
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createLineupModal">צור ליינאפ חדש</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->query("SELECT * FROM lineups ORDER BY created_at DESC");
|
||||||
|
$lineups = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Handle DB error
|
||||||
|
$lineups = [];
|
||||||
|
echo "<div class='alert alert-danger'>שגיאה בטעינת הליינאפים: " . htmlspecialchars($e->getMessage()) . "</div>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if (empty($lineups)): ?>
|
||||||
|
<div class="text-center text-muted py-5">
|
||||||
|
<i class="bi bi-music-note-list" style="font-size: 3rem;"></i>
|
||||||
|
<p class="mt-3">עדיין לא יצרת ליינאפים.</p>
|
||||||
|
<p>לחץ על "צור ליינאפ חדש" כדי להתחיל.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="list-group">
|
||||||
|
<?php foreach ($lineups as $lineup): ?>
|
||||||
|
<a href="lineup_details.php?id=<?php echo $lineup['id']; ?>" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1"><?php echo htmlspecialchars($lineup['name']); ?></h5>
|
||||||
|
<small class="text-muted">נוצר ב: <?php echo date('d/m/Y', strtotime($lineup['created_at'])); ?></small>
|
||||||
|
</div>
|
||||||
|
<i class="bi bi-chevron-left"></i>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Lineup Modal -->
|
||||||
|
<div class="modal fade" id="createLineupModal" tabindex="-1" aria-labelledby="createLineupModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="createLineupModalLabel">יצירת ליינאפ חדש</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="createLineupForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="lineupName" class="form-label">שם הליינאפ</label>
|
||||||
|
<input type="text" class="form-control" id="lineupName" name="lineupName" required>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">סגור</button>
|
||||||
|
<button type="submit" form="createLineupForm" class="btn btn-primary">צור</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||||
77
migrate.php
Normal file
77
migrate.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: text/plain; charset=utf-8');
|
||||||
|
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
echo "Starting database migration...\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// 1. Create migrations tracking table if it doesn't exist
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS `migrations` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`migration_file` VARCHAR(255) NOT NULL,
|
||||||
|
`applied_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY `migration_file_unique` (`migration_file`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||||
|
echo "1. 'migrations' table is ready.\n";
|
||||||
|
|
||||||
|
// 2. Get all migrations that have already been run
|
||||||
|
$stmt = $pdo->query("SELECT `migration_file` FROM `migrations`");
|
||||||
|
$applied_migrations = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
echo "2. Found " . count($applied_migrations) . " applied migrations.\n";
|
||||||
|
|
||||||
|
// 3. Find all migration files on disk
|
||||||
|
$migration_files = glob('db/migrations/*.sql');
|
||||||
|
sort($migration_files);
|
||||||
|
echo "3. Found " . count($migration_files) . " migration files on disk.\n";
|
||||||
|
|
||||||
|
|
||||||
|
// 4. Apply pending migrations
|
||||||
|
$migrations_applied_count = 0;
|
||||||
|
foreach ($migration_files as $file) {
|
||||||
|
$basename = basename($file);
|
||||||
|
if (in_array($basename, $applied_migrations)) {
|
||||||
|
echo "- Skipping already applied migration: {$basename}\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "+ Applying new migration: {$basename}...\n";
|
||||||
|
$sql = file_get_contents($file);
|
||||||
|
if (empty(trim($sql))) {
|
||||||
|
echo " ...file is empty, skipping.\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$pdo->exec($sql);
|
||||||
|
|
||||||
|
$insert_stmt = $pdo->prepare("INSERT INTO `migrations` (migration_file) VALUES (?)");
|
||||||
|
$insert_stmt->execute([$basename]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo " ...SUCCESS!\n";
|
||||||
|
$migrations_applied_count++;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
echo " ...ERROR: " . $e->getMessage() . "\n";
|
||||||
|
echo "\nMigration failed. No changes were made to the database schema.\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($migrations_applied_count > 0) {
|
||||||
|
echo "\nFinished. Applied {" . $migrations_applied_count . "} new migrations successfully.\n";
|
||||||
|
} else {
|
||||||
|
echo "\nFinished. Database is already up to date.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "A critical error occurred: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
274
songs.php
Normal file
274
songs.php
Normal 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'; ?>
|
||||||
Loading…
x
Reference in New Issue
Block a user