diff --git a/composer.json b/composer.json index 254f58f..29f1289 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "license": "MIT", "require": { "php": "^8.3", + "gehrisandro/tailwind-merge-laravel": "^1.4", "laravel/framework": "^13.8", "laravel/socialite": "^5.28", "laravel/tinker": "^3.0", diff --git a/composer.lock b/composer.lock index 7eb26b7..ac7cdb8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "730221cc898aa9b68b8fcb70bee8e84b", + "content-hash": "d8109d7765ee6f6846cd672d742ad3e6", "packages": [ { "name": "brick/math", @@ -644,6 +644,147 @@ ], "time": "2025-12-03T09:33:47+00:00" }, + { + "name": "gehrisandro/tailwind-merge-laravel", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/gehrisandro/tailwind-merge-laravel.git", + "reference": "4dfc54d2f1c148b87a7f9803b3bae74c3437e7d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gehrisandro/tailwind-merge-laravel/zipball/4dfc54d2f1c148b87a7f9803b3bae74c3437e7d9", + "reference": "4dfc54d2f1c148b87a7f9803b3bae74c3437e7d9", + "shasum": "" + }, + "require": { + "gehrisandro/tailwind-merge-php": "^v1.1.0", + "guzzlehttp/guzzle": "^7.5.1", + "laravel/framework": "^12.0|^13.0", + "php": "^8.2.0" + }, + "require-dev": { + "laravel/pint": "^1.13.8", + "orchestra/testbench": "^10.0|^11.0", + "pestphp/pest": "^3.7|^4.4", + "pestphp/pest-plugin-arch": "^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^3.3|^4.0", + "phpstan/phpstan": "^1.10.55|^2.1", + "rector/rector": "^0.19|^2.0", + "symfony/var-dumper": "^6.4.2|^7.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "TailwindMerge\\Laravel\\TailwindMergeServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "TailwindMerge\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "TailwindMerge for Laravel merges multiple Tailwind CSS classes by automatically resolving conflicts between them", + "keywords": [ + "classes", + "laravel", + "merge", + "php", + "tailwindcss" + ], + "support": { + "issues": "https://github.com/gehrisandro/tailwind-merge-laravel/issues", + "source": "https://github.com/gehrisandro/tailwind-merge-laravel/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://github.com/gehrisandro", + "type": "github" + } + ], + "time": "2026-03-21T06:54:49+00:00" + }, + { + "name": "gehrisandro/tailwind-merge-php", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/gehrisandro/tailwind-merge-php.git", + "reference": "400040c89bc958ed130699ee4db0213f689cd498" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gehrisandro/tailwind-merge-php/zipball/400040c89bc958ed130699ee4db0213f689cd498", + "reference": "400040c89bc958ed130699ee4db0213f689cd498", + "shasum": "" + }, + "require": { + "php": "^8.2.0", + "psr/simple-cache": "^3.0" + }, + "require-dev": { + "laravel/pint": "^1.13.8", + "nunomaduro/collision": "^v8.9.1", + "pestphp/pest": "^v4.4.2", + "pestphp/pest-plugin-type-coverage": "^v4.0.3", + "phpstan/phpstan": "^2.1.42", + "rector/rector": "^2.3.9", + "symfony/var-dumper": "^v7.4.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/TailwindMerge.php" + ], + "psr-4": { + "TailwindMerge\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "TailwindMerge for PHP merges multiple Tailwind CSS classes by automatically resolving conflicts between them", + "keywords": [ + "classes", + "merge", + "php", + "tailwindcss" + ], + "support": { + "issues": "https://github.com/gehrisandro/tailwind-merge-php/issues", + "source": "https://github.com/gehrisandro/tailwind-merge-php/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/gehrisandro", + "type": "github" + } + ], + "time": "2026-03-21T06:39:15+00:00" + }, { "name": "graham-campbell/result-type", "version": "v1.1.4", diff --git a/resources/js/candidate-proctor.js b/resources/js/candidate-proctor.js index 4791d6b..b9b4721 100644 --- a/resources/js/candidate-proctor.js +++ b/resources/js/candidate-proctor.js @@ -626,12 +626,12 @@ export function updateScreenShareButton(active) { const btn = document.getElementById('share-screen-btn'); if (!btn) return; if (active) { - btn.innerHTML = '🛑 Stop Screen Sharing'; + btn.innerHTML = ' Stop Screen Sharing'; btn.style.background = '#ef4444'; btn.style.borderColor = '#dc2626'; btn.onclick = stopScreenShare; } else { - btn.innerHTML = '🖥️ Share Screen with Interviewer'; + btn.innerHTML = ' Share Screen with Interviewer'; btn.style.background = 'linear-gradient(135deg, #6366f1 0%, #0078d4 100%)'; btn.style.borderColor = '#6366f1'; btn.onclick = startScreenShare; diff --git a/resources/js/candidate-room.js b/resources/js/candidate-room.js index 7731286..94618ee 100644 --- a/resources/js/candidate-room.js +++ b/resources/js/candidate-room.js @@ -437,22 +437,20 @@ export function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName = if (!tile) { tile = document.createElement('div'); tile.id = 'cand-iv-tile-' + peerId; - tile.className = 'webcam-box'; - tile.style.position = 'relative'; - tile.style.borderColor = '#6366f1'; + tile.className = 'w-full aspect-[16/10] bg-black rounded-xl border-2 border-indigo-500/40 overflow-hidden relative shrink-0'; tile.innerHTML = ` - -
-
+ +
+
${initials}
-
${labelName}
-
Camera Turned Off
+
${labelName}
+
Camera Turned Off
-
- 🎙️ ${labelName} - | +
+ ${labelName} + |
@@ -477,7 +475,13 @@ export function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName = camIcon.title = camOnVal ? 'Camera On' : 'Camera Off'; } if (placeholderEl) { - placeholderEl.style.display = camOnVal ? 'none' : 'flex'; + if (camOnVal) { + placeholderEl.classList.add('hidden'); + placeholderEl.classList.remove('flex'); + } else { + placeholderEl.classList.remove('hidden'); + placeholderEl.classList.add('flex'); + } } } @@ -492,7 +496,7 @@ export function addOrUpdateCandidateInterviewerTile(peerId, stream, labelName = const badge = document.getElementById('call-status-badge'); if (badge) { - badge.innerText = '🟢 CALL CONNECTED (LIVE)'; + badge.innerHTML = ' CALL LIVE'; badge.style.background = 'rgba(16,185,129,0.3)'; badge.style.color = '#34d399'; } @@ -516,11 +520,11 @@ export function removeCandidateInterviewerTile(peerId) { const badge = document.getElementById('call-status-badge'); if (badge) { if (latestCallStatus === 'ended') { - badge.innerText = 'CALL ENDED BY HOST'; + badge.innerText = 'CALL ENDED'; badge.style.background = 'rgba(239,68,68,0.2)'; badge.style.color = '#fca5a5'; } else { - badge.innerText = 'READY (Waiting for Interviewer)'; + badge.innerText = 'READY'; badge.style.background = 'rgba(16,185,129,0.2)'; badge.style.color = '#34d399'; } @@ -550,7 +554,7 @@ export function triggerCandidateRing(offer = null, callStartedAt = null) { const badge = document.getElementById('call-status-badge'); if (badge) { - badge.innerText = '🔔 INCOMING CALL... RINGING'; + badge.innerHTML = ' INCOMING CALL... RINGING'; badge.style.background = 'rgba(234,179,8,0.3)'; badge.style.color = '#fde047'; } @@ -618,7 +622,7 @@ export function showCandidateJoinOption() { } const badge = document.getElementById('call-status-badge'); if (badge) { - badge.innerText = '📞 CALL IN PROGRESS'; + badge.innerText = 'CALL IN PROGRESS'; badge.style.background = 'rgba(59,130,246,0.3)'; badge.style.color = '#93c5fd'; } @@ -626,7 +630,7 @@ export function showCandidateJoinOption() { if (joinBtn) { joinBtn.style.display = 'block'; joinBtn.disabled = false; - joinBtn.innerText = '🟢 📞 Call in Progress • Join Call Now'; + joinBtn.innerHTML = ' Join Call Now'; } } @@ -679,7 +683,7 @@ export async function acceptCandidateCall() { const badge = document.getElementById('call-status-badge'); if (badge) { - badge.innerText = '🟢 CALL CONNECTED (LIVE)'; + badge.innerText = 'CALL CONNECTED'; badge.style.background = 'rgba(16,185,129,0.3)'; badge.style.color = '#34d399'; } @@ -791,7 +795,7 @@ export function candidateLeaveCall(isEndedByHost = false) { } } else { if (joinBtn) { - joinBtn.innerText = '🟢 📞 Rejoin Call Now'; + joinBtn.innerHTML = ' Rejoin Call Now'; joinBtn.style.display = 'block'; joinBtn.disabled = false; } @@ -1088,9 +1092,10 @@ export function toggleMic() { audioTracks[0].enabled = isCandidateMicEnabled; const btn = document.getElementById('toggle-mic-btn'); if (btn) { - btn.innerText = isCandidateMicEnabled ? '🎤 Mic On' : '🔇 Mic Muted'; + btn.innerHTML = ` ${isCandidateMicEnabled ? 'Mic On' : 'Mic Off'}`; btn.style.background = isCandidateMicEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)'; btn.style.borderColor = isCandidateMicEnabled ? '#10b981' : '#ef4444'; + btn.className = `flex-1 py-2 px-2 text-xs font-semibold rounded-lg ${isCandidateMicEnabled ? 'bg-emerald-500/20 border-emerald-500 text-emerald-300 hover:bg-emerald-500/30' : 'bg-red-500/20 border-red-500 text-red-300 hover:bg-red-500/30'} border transition-all cursor-pointer flex items-center justify-center gap-1.5`; } const icon = document.getElementById('cand-self-mic-icon'); if (icon) { @@ -1111,9 +1116,10 @@ export function toggleCam() { videoTracks[0].enabled = isCandidateCamEnabled; const btn = document.getElementById('toggle-cam-btn'); if (btn) { - btn.innerText = isCandidateCamEnabled ? '📷 Cam On' : '🚫 Cam Off'; + btn.innerHTML = ` ${isCandidateCamEnabled ? 'Cam On' : 'Cam Off'}`; btn.style.background = isCandidateCamEnabled ? 'rgba(16,185,129,0.2)' : 'rgba(239,68,68,0.2)'; btn.style.borderColor = isCandidateCamEnabled ? '#10b981' : '#ef4444'; + btn.className = `flex-1 py-2 px-2 text-xs font-semibold rounded-lg ${isCandidateCamEnabled ? 'bg-emerald-500/20 border-emerald-500 text-emerald-300 hover:bg-emerald-500/30' : 'bg-red-500/20 border-red-500 text-red-300 hover:bg-red-500/30'} border transition-all cursor-pointer flex items-center justify-center gap-1.5`; } if (avatarPlaceholder) avatarPlaceholder.style.display = isCandidateCamEnabled ? 'none' : 'flex'; } diff --git a/resources/js/interview-call.js b/resources/js/interview-call.js index a3baa1c..b8bdd93 100644 --- a/resources/js/interview-call.js +++ b/resources/js/interview-call.js @@ -362,8 +362,71 @@ export function updateScreenshotToggleBtnUI(enabled) { } } -export function switchIdeTab(tab) { - const tabs = ['code', 'notes', 'drawing', 'recordings', 'screenshots']; +let currentInspectorTab = null; + +export function closeInspectorPanel() { + currentInspectorTab = null; + try { + if (currentInterviewId) { + sessionStorage.removeItem('interview_inspector_tab_' + currentInterviewId); + } + } catch(e) {} + + const details = document.getElementById('inspector-details'); + if (details) details.open = false; + + const tabs = ['logs', 'code', 'notes', 'drawing', 'recordings', 'screenshots']; + tabs.forEach(t => { + const contentEl = document.getElementById(`tab-content-${t}`); + const btnEl = document.getElementById(`tab-btn-${t}`); + if (contentEl) contentEl.style.display = 'none'; + if (btnEl) { + btnEl.classList.remove('bg-indigo-600', 'text-white'); + btnEl.classList.add('text-slate-400', 'hover:bg-white/5'); + } + }); +} + +export function switchIdeTab(tab, event = null) { + if (event) { + event.stopPropagation(); + } + + const details = document.getElementById('inspector-details'); + + // If user clicks the currently active open tab button, toggle it collapsed + if (currentInspectorTab === tab && details && details.open) { + closeInspectorPanel(); + return; + } + + currentInspectorTab = tab; + try { + if (currentInterviewId) { + sessionStorage.setItem('interview_inspector_tab_' + currentInterviewId, tab); + } + } catch(e) {} + + if (details) details.open = true; + + const tabs = ['logs', 'code', 'notes', 'drawing', 'recordings', 'screenshots']; + const tabMeta = { + logs: { title: 'Proctoring Logs', icon: 'fa-solid fa-shield-halved' }, + notes: { title: 'Candidate Notepad', icon: 'fa-solid fa-note-sticky' }, + drawing: { title: 'Candidate Drawing Board', icon: 'fa-solid fa-pen' }, + recordings: { title: 'Saved Video Recordings', icon: 'fa-solid fa-film' }, + screenshots: { title: 'Tab Switch Screenshots', icon: 'fa-solid fa-image' }, + code: { title: 'Candidate Code', icon: 'fa-solid fa-code' } + }; + + const floatingTitle = document.getElementById('inspector-floating-text'); + const floatingIcon = document.getElementById('inspector-floating-icon'); + + if (tabMeta[tab]) { + if (floatingTitle) floatingTitle.innerText = tabMeta[tab].title; + if (floatingIcon) floatingIcon.className = `${tabMeta[tab].icon} text-indigo-400`; + } + tabs.forEach(t => { const contentEl = document.getElementById(`tab-content-${t}`); const btnEl = document.getElementById(`tab-btn-${t}`); @@ -372,16 +435,26 @@ export function switchIdeTab(tab) { } if (btnEl) { if (t === tab) { - btnEl.classList.add('bg-indigo-600', 'text-white', 'border-indigo-500'); - btnEl.classList.remove('bg-white/5', 'text-slate-400', 'border-slate-700/60'); + btnEl.classList.add('bg-indigo-600', 'text-white'); + btnEl.classList.remove('text-slate-400', 'hover:bg-white/5'); } else { - btnEl.classList.remove('bg-indigo-600', 'text-white', 'border-indigo-500'); - btnEl.classList.add('bg-white/5', 'text-slate-400', 'border-slate-700/60'); + btnEl.classList.remove('bg-indigo-600', 'text-white'); + btnEl.classList.add('text-slate-400', 'hover:bg-white/5'); } } }); } +// Click outside to dismiss floating inspector details +if (typeof document !== 'undefined') { + document.addEventListener('click', function(e) { + const details = document.getElementById('inspector-details'); + if (details && details.open && !details.contains(e.target)) { + closeInspectorPanel(); + } + }); +} + export function triggerIncomingCallRing(callData, isRealtimeBroadcast = false) { if (!callData || !callData.id) return; if (typeof document !== 'undefined' && document.body && document.body.dataset.interviewId) return; @@ -609,7 +682,24 @@ export function openReviewModal(id, name, uid, autoJoinCall = false) { if (reviewModal) reviewModal.classList.add('active'); resetZoomScreen(); - switchIdeTab('code'); + if (document.getElementById('inspector-content-container')) { + let savedInspectorTab = null; + try { + savedInspectorTab = sessionStorage.getItem('interview_inspector_tab_' + id); + } catch(e) {} + + if (savedInspectorTab) { + switchIdeTab(savedInspectorTab); + } else { + closeInspectorPanel(); + } + } else if (document.getElementById('tab-btn-logs')) { + switchIdeTab('logs'); + } else if (document.getElementById('tab-btn-notes')) { + switchIdeTab('notes'); + } else { + switchIdeTab('code'); + } updateCandidateStatusIcons(false, false, false); initInterviewerSigChannel(); @@ -783,6 +873,9 @@ export function pollReviewData() { const screenPlace = document.getElementById('screen-video-placeholder'); if (screenVid) screenVid.srcObject = null; if (screenPlace) screenPlace.style.display = 'flex'; + if (typeof window.updateScreenShareLayout === 'function') { + window.updateScreenShareLayout(false); + } panelistMeshTiles.forEach((_, pid) => { removePanelistTile(pid); @@ -957,7 +1050,7 @@ export function pollReviewData() { const badgeLabel = isCand ? 'Candidate' : 'Panelist'; const badgeClass = isCand ? 'bg-sky-500/20 text-sky-300 border-sky-500/30' : 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30'; - chatHtml += `
+ chatHtml += `
@@ -969,11 +1062,11 @@ export function pollReviewData() {
${w.message}
`; }); - chatHistory.className = 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] h-[150px] overflow-y-auto p-3 flex flex-col gap-2 mb-2.5'; + chatHistory.className = 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] h-full overflow-y-auto p-3 flex flex-col gap-2 mb-2.5'; chatHistory.innerHTML = chatHtml; chatHistory.scrollTop = chatHistory.scrollHeight; } else { - chatHistory.className = 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] h-[150px] overflow-y-auto p-3 mb-2.5'; + chatHistory.className = 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] h-full overflow-y-auto p-3 mb-2.5'; chatHistory.innerHTML = '
No chat history. Send a message below.
'; } } @@ -1419,7 +1512,7 @@ export function addOrUpdatePanelistTile(peerId, stream, name = 'Panelist', micOn if (!tile) { tile = document.createElement('div'); tile.id = 'panelist-tile-' + peerId; - tile.className = 'video-tile relative aspect-[16/11] bg-[#0d1220] border border-[#1b2233] rounded-[10px] overflow-hidden flex flex-col items-center justify-center transition-all'; + tile.className = 'video-tile relative w-[220px] aspect-video shrink-0 bg-[#0d1220] border border-[#1b2233] rounded-[10px] overflow-hidden flex flex-col items-center justify-center transition-all'; tile.innerHTML = ` @@ -1513,6 +1606,18 @@ export async function initInterviewerPeer() { console.log('[WebRTC Screen] Attaching incoming screen stream to video element'); const screenVid = document.getElementById('interviewer-screen-video'); const placeholder = document.getElementById('screen-video-placeholder'); + if (typeof window.updateScreenShareLayout === 'function') { + window.updateScreenShareLayout(true); + } + if (screenStream && screenStream.getVideoTracks) { + screenStream.getVideoTracks().forEach(track => { + track.onended = () => { + if (typeof window.updateScreenShareLayout === 'function') { + window.updateScreenShareLayout(false); + } + }; + }); + } if (screenVid && screenStream) { screenVid.muted = true; if (screenVid.srcObject !== screenStream) { @@ -2355,6 +2460,7 @@ if (typeof window !== 'undefined') { window.closeReviewModal = closeReviewModal; window.sendSilentWarning = sendSilentWarning; window.setCurrentInterviewId = setCurrentInterviewId; + window.closeInspectorPanel = closeInspectorPanel; // Attach listeners ['click', 'keydown', 'touchstart'].forEach(evt => { diff --git a/resources/views/components/audit-log.blade.php b/resources/views/components/audit-log.blade.php index 21000a0..3e047f2 100644 --- a/resources/views/components/audit-log.blade.php +++ b/resources/views/components/audit-log.blade.php @@ -2,7 +2,7 @@ 'logs' => [], ]) -
merge(['class' => 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] max-h-[260px] overflow-y-auto']) }}> +
twMerge(['class' => 'bg-[#0d1220] border border-[#1b2233] rounded-[9px] overflow-y-auto']) }}> @if(is_array($logs) && count($logs) > 0) @foreach($logs as $log) merge(['class' => $classes]) }}> + +
+ + +
+ {{ $slot }} +
+ + + @if(isset($footer)) +
+ {{ $footer }} +
+ @endif +
+
diff --git a/resources/views/components/interview/call-controls.blade.php b/resources/views/components/interview/call-controls.blade.php index c331795..6a7e891 100644 --- a/resources/views/components/interview/call-controls.blade.php +++ b/resources/views/components/interview/call-controls.blade.php @@ -28,7 +28,7 @@ - +