diff --git a/add_song.php b/add_song.php
new file mode 100644
index 0000000..91492d4
--- /dev/null
+++ b/add_song.php
@@ -0,0 +1,82 @@
+ 'error',
+ 'message' => 'Invalid request.'
+];
+
+// Get the posted data
+$data = json_decode(file_get_contents('php://input'), true);
+
+if (!$data) {
+ echo json_encode($response);
+ exit();
+}
+
+// Basic validation
+$session_code = $data['sessionCode'] ?? null;
+$track_title = $data['title'] ?? null;
+$artist_name = $data['artist'] ?? null;
+$album_art_url = $data['albumArt'] ?? null;
+$source = $data['source'] ?? null;
+
+if (!$session_code || !$track_title || !$artist_name) {
+ $response['message'] = 'Missing required song data.';
+ echo json_encode($response);
+ exit();
+}
+
+try {
+ $pdo = db();
+
+ // 1. Get session ID from session code
+ $stmt = $pdo->prepare("SELECT id FROM sessions WHERE session_code = ? AND status = 'active'");
+ $stmt->execute([$session_code]);
+ $session = $stmt->fetch();
+
+ if (!$session) {
+ $response['message'] = 'Invalid or inactive session.';
+ echo json_encode($response);
+ exit();
+ }
+ $session_id = $session['id'];
+
+ // 2. Check for duplicates
+ $stmt = $pdo->prepare("SELECT id FROM queue_items WHERE session_id = ? AND track_title = ? AND artist_name = ?");
+ $stmt->execute([$session_id, $track_title, $artist_name]);
+ if ($stmt->fetch()) {
+ $response['message'] = 'This song is already in the queue.';
+ echo json_encode($response);
+ exit();
+ }
+
+ // 3. Insert the new song
+ $sql = "INSERT INTO queue_items (session_id, track_title, artist_name, album_art_url, source, added_by) VALUES (?, ?, ?, ?, ?, ?)";
+ $stmt = $pdo->prepare($sql);
+
+ // For now, added_by is anonymous. This can be updated later with user profiles.
+ $added_by = 'Guest';
+
+ if ($stmt->execute([$session_id, $track_title, $artist_name, $album_art_url, $source, $added_by])) {
+ $new_song_id = $pdo->lastInsertId();
+
+ // Fetch the newly added song to return to the client
+ $stmt = $pdo->prepare("SELECT * FROM queue_items WHERE id = ?");
+ $stmt->execute([$new_song_id]);
+ $new_song_data = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ $response['status'] = 'success';
+ $response['message'] = 'Song added successfully!';
+ $response['data'] = $new_song_data;
+ } else {
+ $response['message'] = 'Failed to add song to the database.';
+ }
+
+} catch (PDOException $e) {
+ // In a real app, log this error instead of exposing it.
+ $response['message'] = 'Database error: ' . $e->getMessage();
+}
+
+echo json_encode($response);
diff --git a/assets/css/custom.css b/assets/css/custom.css
index dd5a65d..f1d4508 100644
--- a/assets/css/custom.css
+++ b/assets/css/custom.css
@@ -118,3 +118,237 @@ body {
.footer a:hover {
color: var(--primary-color);
}
+
+/* Floating Action Button (FAB) */
+.fab {
+ position: fixed;
+ bottom: 30px;
+ right: 30px;
+ width: 60px;
+ height: 60px;
+ background-color: var(--primary-color);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-primary-color);
+ font-size: 24px;
+ border: none;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ cursor: pointer;
+ transition: transform 0.2s ease-in-out, background-color 0.2s;
+ z-index: 1050;
+}
+
+.fab:hover {
+ background-color: #1ed760;
+ transform: scale(1.1);
+}
+
+/* Modal Styles */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(5px);
+ z-index: 1055;
+ display: none; /* Hidden by default */
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.modal-overlay.visible {
+ display: block;
+ opacity: 1;
+}
+
+.modal-container {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ background-color: var(--surface-color);
+ border-top-left-radius: 20px;
+ border-top-right-radius: 20px;
+ padding: 1.5rem;
+ transform: translateY(100%);
+ transition: transform 0.3s ease-out;
+ max-height: 80vh;
+ overflow-y: auto;
+}
+
+.modal-overlay.visible .modal-container {
+ transform: translateY(0);
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid #282828;
+}
+
+.modal-title {
+ font-weight: 700;
+ font-size: 1.5rem;
+}
+
+.btn-close-modal {
+ background: none;
+ border: none;
+ color: var(--text-secondary-color);
+ font-size: 1.5rem;
+}
+
+.btn-close-modal:hover {
+ color: var(--text-primary-color);
+}
+
+.search-bar-container {
+ position: relative;
+ margin-top: 1rem;
+}
+
+.search-bar-container .bi-search {
+ position: absolute;
+ top: 50%;
+ left: 15px;
+ transform: translateY(-50%);
+ color: var(--text-secondary-color);
+}
+
+#song-search-input {
+ background-color: #282828;
+ border: 1px solid #333;
+ color: var(--text-primary-color);
+ border-radius: 8px;
+ padding-left: 40px;
+ height: 50px;
+}
+
+#song-search-input::placeholder {
+ color: var(--text-secondary-color);
+}
+
+.source-toggles {
+ display: flex;
+ gap: 10px;
+ margin-top: 1rem;
+}
+
+.btn-source {
+ flex-grow: 1;
+ background-color: #282828;
+ border: 1px solid #333;
+ color: var(--text-secondary-color);
+ border-radius: 8px;
+ padding: 10px;
+ transition: background-color 0.2s, color 0.2s;
+}
+
+.btn-source.active {
+ background-color: var(--primary-color);
+ color: var(--text-primary-color);
+ border-color: var(--primary-color);
+}
+
+.search-result-item {
+ display: flex;
+ align-items: center;
+ padding: 10px;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.search-result-item:hover {
+ background-color: #282828;
+}
+
+.search-result-item img {
+ width: 50px;
+ height: 50px;
+ border-radius: 4px;
+ margin-right: 15px;
+}
+
+.search-result-item .track-info {
+ flex-grow: 1;
+}
+
+.search-result-item .track-title {
+ font-weight: 600;
+}
+
+.search-result-item .artist-name {
+ font-size: 0.9rem;
+ color: var(--text-secondary-color);
+}
+
+.source-icon {
+ font-size: 1.5rem;
+ color: var(--text-secondary-color);
+}
+
+.queue-item {
+ background-color: var(--surface-color);
+ border-radius: var(--border-radius-card);
+ padding: 1rem;
+ margin-bottom: 1rem;
+ display: flex;
+ align-items: center;
+ border: 1px solid #282828;
+ opacity: 1;
+ transform: translateY(0);
+ transition: opacity 0.5s ease, transform 0.5s ease;
+}
+
+.queue-item.new-item {
+ opacity: 0;
+ transform: translateY(20px);
+}
+
+
+.queue-item img {
+ width: 64px;
+ height: 64px;
+ border-radius: 8px;
+ margin-right: 1rem;
+}
+
+.queue-item .track-info {
+ flex-grow: 1;
+}
+
+.queue-item .track-title {
+ font-weight: 600;
+ color: var(--text-primary-color);
+}
+
+.queue-item .artist-name {
+ color: var(--text-secondary-color);
+}
+
+.vote-button {
+ background-color: transparent;
+ border: 1px solid var(--primary-color);
+ color: var(--primary-color);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 0.5rem;
+ border-radius: 8px;
+ font-weight: 600;
+ width: 50px;
+ height: 50px;
+}
+
+.vote-button .bi {
+ font-size: 1.2rem;
+}
+
diff --git a/assets/js/main.js b/assets/js/main.js
index bf84413..060097b 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -10,4 +10,103 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
});
+
+ // --- Add Song Modal Logic ---
+ const addSongFab = document.getElementById('add-song-fab');
+ const modalOverlay = document.getElementById('add-song-modal-overlay');
+ const modalContainer = document.getElementById('add-song-modal-container');
+ const closeModalBtn = document.getElementById('close-modal-btn');
+ const searchResultsContainer = document.getElementById('search-results');
+ const queueList = document.getElementById('queue-list');
+ const emptyQueueMessage = document.getElementById('empty-queue-message');
+
+ if (addSongFab) {
+ addSongFab.addEventListener('click', () => {
+ modalOverlay.classList.add('visible');
+ });
+ }
+
+ if (closeModalBtn) {
+ closeModalBtn.addEventListener('click', closeModal);
+ }
+
+ if (modalOverlay) {
+ modalOverlay.addEventListener('click', (e) => {
+ if (e.target === modalOverlay) {
+ closeModal();
+ }
+ });
+ }
+
+ function closeModal() {
+ modalContainer.style.transform = 'translateY(100%)';
+ modalOverlay.classList.remove('visible');
+ }
+
+ // --- Add Song to Queue Logic ---
+ searchResultsContainer.addEventListener('click', (e) => {
+ const resultItem = e.target.closest('.search-result-item');
+ if (!resultItem) return;
+
+ const song = {
+ title: resultItem.dataset.title,
+ artist: resultItem.dataset.artist,
+ albumArt: resultItem.dataset.albumArt,
+ source: resultItem.dataset.source,
+ sessionCode: window.currentSessionCode
+ };
+
+ addSongToQueue(song);
+ });
+
+ async function addSongToQueue(song) {
+ try {
+ const response = await fetch('add_song.php', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(song),
+ });
+
+ const result = await response.json();
+
+ if (result.status === 'success') {
+ addSongToDOM(result.data);
+ closeModal();
+ } else {
+ alert(result.message || 'Failed to add song.');
+ }
+ } catch (error) {
+ console.error('Error adding song:', error);
+ alert('An error occurred. Please try again.');
+ }
+ }
+
+ function addSongToDOM(song) {
+ if (emptyQueueMessage) {
+ emptyQueueMessage.remove();
+ }
+
+ const queueItem = document.createElement('div');
+ queueItem.className = 'queue-item new-item';
+ queueItem.innerHTML = `
+
+
Be the first to add a song and get the party started!
+