Spaces:
Sleeping
Sleeping
Commit ·
b66d14f
1
Parent(s): 9d6cc86
Fix messaging UI alignment, unsend functionality, and applicant notification bugs
Browse files
src/components/Admin/AdminInterviewManagement.jsx
CHANGED
|
@@ -2,13 +2,13 @@ import React, { useState, useEffect, useRef } from 'react';
|
|
| 2 |
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
import { supabase } from '../../supabaseClient';
|
| 4 |
import FullProfileOverlay from '../FullProfileOverlay';
|
| 5 |
-
import ScheduleInterviewModal from '../ScheduleInterviewModal';
|
| 6 |
|
| 7 |
// --- ICONS ---
|
| 8 |
const SmallCalendarIcon = () => (<svg style={{ width: '24px', height: '24px', color: 'rgba(255,255,255,0.7)' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" /></svg>);
|
| 9 |
const ChevronRightIcon = () => (<svg style={{ width: '16px', height: '16px', marginLeft: '4px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /></svg>);
|
| 10 |
const CloseIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>;
|
| 11 |
-
const SendIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>;
|
| 12 |
|
| 13 |
// --- NEW FULL CHAT UI MODAL ---
|
| 14 |
const AdminChatModal = ({ isOpen, onClose, applicant }) => {
|
|
@@ -78,20 +78,56 @@ const AdminChatModal = ({ isOpen, onClose, applicant }) => {
|
|
| 78 |
if (error) console.error("Error sending message:", error);
|
| 79 |
};
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
if (!isOpen || !applicant) return null;
|
| 82 |
|
| 83 |
return (
|
| 84 |
<div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(5px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }}>
|
| 85 |
<style>{`.chat-scroll::-webkit-scrollbar { width: 6px; } .chat-scroll::-webkit-scrollbar-track { background: transparent; } .chat-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }`}</style>
|
| 86 |
-
|
| 87 |
-
<motion.div
|
| 88 |
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 89 |
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 90 |
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 91 |
-
style={{
|
| 92 |
-
width: '100%', maxWidth: '600px', height: '75vh',
|
| 93 |
-
background: 'rgba(15, 23, 42, 0.95)', border: '1px solid rgba(255,255,255,0.1)',
|
| 94 |
-
borderRadius: '1.25rem', display: 'flex', flexDirection: 'column',
|
| 95 |
overflow: 'hidden', boxShadow: '0 25px 50px rgba(0,0,0,0.5)'
|
| 96 |
}}
|
| 97 |
>
|
|
@@ -121,19 +157,26 @@ const AdminChatModal = ({ isOpen, onClose, applicant }) => {
|
|
| 121 |
messages.map(m => {
|
| 122 |
const isMe = m.sender_id === adminId;
|
| 123 |
return (
|
| 124 |
-
<motion.div key={m.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} style={{ alignSelf: isMe ? 'flex-end' : 'flex-start', maxWidth: '75%' }}>
|
| 125 |
-
<div style={{
|
| 126 |
// Use Admin's Red Theme for outgoing bubbles
|
| 127 |
-
background: isMe ? 'linear-gradient(135deg, #EF4444 0%, #DC2626 100%)' : 'rgba(255,255,255,0.08)',
|
| 128 |
-
color: 'white', padding: '0.85rem 1.25rem', borderRadius: '1.25rem',
|
| 129 |
borderBottomRightRadius: isMe ? 4 : '1.25rem', borderBottomLeftRadius: isMe ? '1.25rem' : 4,
|
| 130 |
fontSize: '0.95rem', lineHeight: '1.5',
|
| 131 |
boxShadow: isMe ? '0 4px 15px rgba(239, 68, 68, 0.3)' : 'none'
|
| 132 |
}}>
|
| 133 |
{m.content}
|
| 134 |
</div>
|
| 135 |
-
<div style={{
|
| 136 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
</div>
|
| 138 |
</motion.div>
|
| 139 |
);
|
|
@@ -145,19 +188,19 @@ const AdminChatModal = ({ isOpen, onClose, applicant }) => {
|
|
| 145 |
{/* Input Area */}
|
| 146 |
<div style={{ padding: '1.25rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
| 147 |
<div style={{ display: 'flex', gap: '0.75rem', background: 'rgba(255,255,255,0.03)', borderRadius: '1rem', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.1)' }}>
|
| 148 |
-
<textarea
|
| 149 |
-
value={text}
|
| 150 |
-
onChange={e => setText(e.target.value)}
|
| 151 |
-
onKeyDown={e => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }}}
|
| 152 |
-
placeholder="Type a message..."
|
| 153 |
-
style={{ flex: 1, padding: '0.75rem', border: 'none', background: 'transparent', color: 'white', outline: 'none', resize: 'none', height: '45px', fontFamily: 'inherit' }}
|
| 154 |
/>
|
| 155 |
-
<button
|
| 156 |
-
onClick={sendMsg}
|
| 157 |
-
disabled={!text.trim()}
|
| 158 |
-
style={{
|
| 159 |
-
background: text.trim() ? '#EF4444' : 'rgba(255,255,255,0.05)',
|
| 160 |
-
color: text.trim() ? 'white' : '#64748b', border: 'none', borderRadius: '50%',
|
| 161 |
width: '45px', height: '45px', cursor: text.trim() ? 'pointer' : 'not-allowed',
|
| 162 |
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
| 163 |
transition: 'all 0.2s ease'
|
|
@@ -179,7 +222,7 @@ export default function AdminInterviewManagement() {
|
|
| 179 |
const [applicants, setApplicants] = useState({ interviews: [], accepted: [], rejected: [] });
|
| 180 |
|
| 181 |
// Modals State
|
| 182 |
-
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
|
| 183 |
const [isMessageModalOpen, setIsMessageModalOpen] = useState(false);
|
| 184 |
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
| 185 |
|
|
@@ -271,8 +314,8 @@ export default function AdminInterviewManagement() {
|
|
| 271 |
};
|
| 272 |
|
| 273 |
// 1. Fetch Data on Component Mount
|
| 274 |
-
useEffect(() => {
|
| 275 |
-
fetchData();
|
| 276 |
}, []);
|
| 277 |
|
| 278 |
// 2. Updated Schedule Handler
|
|
@@ -314,7 +357,7 @@ export default function AdminInterviewManagement() {
|
|
| 314 |
location: mode === 'Offline' ? details : null,
|
| 315 |
interviewer_name: interviewerName,
|
| 316 |
interviewer_role: interviewerRole,
|
| 317 |
-
duration_mins: 45
|
| 318 |
};
|
| 319 |
|
| 320 |
let dbError;
|
|
@@ -444,19 +487,19 @@ export default function AdminInterviewManagement() {
|
|
| 444 |
|
| 445 |
<AnimatePresence>
|
| 446 |
{isMessageModalOpen && (
|
| 447 |
-
<AdminChatModal
|
| 448 |
-
isOpen={isMessageModalOpen}
|
| 449 |
-
onClose={() => setIsMessageModalOpen(false)}
|
| 450 |
-
applicant={selectedApplicant}
|
| 451 |
/>
|
| 452 |
)}
|
| 453 |
</AnimatePresence>
|
| 454 |
-
|
| 455 |
<AnimatePresence>
|
| 456 |
{isDrawerOpen && (
|
| 457 |
-
<FullProfileOverlay
|
| 458 |
-
candidate={drawerCandidate}
|
| 459 |
-
onClose={() => setIsDrawerOpen(false)}
|
| 460 |
/>
|
| 461 |
)}
|
| 462 |
</AnimatePresence>
|
|
|
|
| 2 |
import { motion, AnimatePresence } from 'framer-motion';
|
| 3 |
import { supabase } from '../../supabaseClient';
|
| 4 |
import FullProfileOverlay from '../FullProfileOverlay';
|
| 5 |
+
import ScheduleInterviewModal from '../ScheduleInterviewModal';
|
| 6 |
|
| 7 |
// --- ICONS ---
|
| 8 |
const SmallCalendarIcon = () => (<svg style={{ width: '24px', height: '24px', color: 'rgba(255,255,255,0.7)' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" /></svg>);
|
| 9 |
const ChevronRightIcon = () => (<svg style={{ width: '16px', height: '16px', marginLeft: '4px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /></svg>);
|
| 10 |
const CloseIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>;
|
| 11 |
+
const SendIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" /></svg>;
|
| 12 |
|
| 13 |
// --- NEW FULL CHAT UI MODAL ---
|
| 14 |
const AdminChatModal = ({ isOpen, onClose, applicant }) => {
|
|
|
|
| 78 |
if (error) console.error("Error sending message:", error);
|
| 79 |
};
|
| 80 |
|
| 81 |
+
const unsendMsg = async (msgId) => {
|
| 82 |
+
if (!window.confirm("Unsend this message?")) return;
|
| 83 |
+
|
| 84 |
+
// 1. Try to delete the message
|
| 85 |
+
const { data, error } = await supabase.from('messages').delete().eq('id', msgId).select();
|
| 86 |
+
|
| 87 |
+
if (error) {
|
| 88 |
+
console.error("Unsend Error:", error);
|
| 89 |
+
alert("Failed to unsend message.");
|
| 90 |
+
return;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// 2. If RLS silently blocks deletion, fallback to update
|
| 94 |
+
if (!data || data.length === 0) {
|
| 95 |
+
const { error: updateError } = await supabase
|
| 96 |
+
.from('messages')
|
| 97 |
+
.update({ content: "🚫 This message was unsent" })
|
| 98 |
+
.eq('id', msgId);
|
| 99 |
+
|
| 100 |
+
if (updateError) {
|
| 101 |
+
alert("Database policies prevent unsending this message.");
|
| 102 |
+
return;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
setMessages(prev => prev.map(m => m.id === msgId ? { ...m, content: "🚫 This message was unsent" } : m));
|
| 106 |
+
} else {
|
| 107 |
+
setMessages(prev => prev.filter(m => m.id !== msgId));
|
| 108 |
+
}
|
| 109 |
+
};
|
| 110 |
+
|
| 111 |
+
const canUnsend = (rawTime) => {
|
| 112 |
+
if (!rawTime) return false;
|
| 113 |
+
const msgTime = new Date(rawTime).getTime();
|
| 114 |
+
return (Date.now() - msgTime) < 5 * 60 * 1000; // 5 minutes
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
if (!isOpen || !applicant) return null;
|
| 118 |
|
| 119 |
return (
|
| 120 |
<div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(5px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }}>
|
| 121 |
<style>{`.chat-scroll::-webkit-scrollbar { width: 6px; } .chat-scroll::-webkit-scrollbar-track { background: transparent; } .chat-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }`}</style>
|
| 122 |
+
|
| 123 |
+
<motion.div
|
| 124 |
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 125 |
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 126 |
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 127 |
+
style={{
|
| 128 |
+
width: '100%', maxWidth: '600px', height: '75vh',
|
| 129 |
+
background: 'rgba(15, 23, 42, 0.95)', border: '1px solid rgba(255,255,255,0.1)',
|
| 130 |
+
borderRadius: '1.25rem', display: 'flex', flexDirection: 'column',
|
| 131 |
overflow: 'hidden', boxShadow: '0 25px 50px rgba(0,0,0,0.5)'
|
| 132 |
}}
|
| 133 |
>
|
|
|
|
| 157 |
messages.map(m => {
|
| 158 |
const isMe = m.sender_id === adminId;
|
| 159 |
return (
|
| 160 |
+
<motion.div key={m.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} style={{ alignSelf: isMe ? 'flex-end' : 'flex-start', maxWidth: '75%', position: 'relative' }}>
|
| 161 |
+
<div style={{
|
| 162 |
// Use Admin's Red Theme for outgoing bubbles
|
| 163 |
+
background: isMe ? 'linear-gradient(135deg, #EF4444 0%, #DC2626 100%)' : 'rgba(255,255,255,0.08)',
|
| 164 |
+
color: 'white', padding: '0.85rem 1.25rem', borderRadius: '1.25rem',
|
| 165 |
borderBottomRightRadius: isMe ? 4 : '1.25rem', borderBottomLeftRadius: isMe ? '1.25rem' : 4,
|
| 166 |
fontSize: '0.95rem', lineHeight: '1.5',
|
| 167 |
boxShadow: isMe ? '0 4px 15px rgba(239, 68, 68, 0.3)' : 'none'
|
| 168 |
}}>
|
| 169 |
{m.content}
|
| 170 |
</div>
|
| 171 |
+
<div style={{ display: 'flex', justifyContent: isMe ? 'flex-end' : 'flex-start', alignItems: 'center', gap: '8px', marginTop: '4px' }}>
|
| 172 |
+
<div style={{ fontSize: '0.7rem', color: '#64748b', padding: '0 0.5rem' }}>
|
| 173 |
+
{new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
| 174 |
+
</div>
|
| 175 |
+
{isMe && canUnsend(m.created_at) && (
|
| 176 |
+
<button onClick={() => unsendMsg(m.id)} title="Unsend message (5 min window)" style={{ background: 'transparent', border: 'none', color: '#ef4444', cursor: 'pointer', padding: '0 2px', display: 'flex', alignItems: 'center' }}>
|
| 177 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
| 178 |
+
</button>
|
| 179 |
+
)}
|
| 180 |
</div>
|
| 181 |
</motion.div>
|
| 182 |
);
|
|
|
|
| 188 |
{/* Input Area */}
|
| 189 |
<div style={{ padding: '1.25rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
| 190 |
<div style={{ display: 'flex', gap: '0.75rem', background: 'rgba(255,255,255,0.03)', borderRadius: '1rem', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.1)' }}>
|
| 191 |
+
<textarea
|
| 192 |
+
value={text}
|
| 193 |
+
onChange={e => setText(e.target.value)}
|
| 194 |
+
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); } }}
|
| 195 |
+
placeholder="Type a message..."
|
| 196 |
+
style={{ flex: 1, padding: '0.75rem', border: 'none', background: 'transparent', color: 'white', outline: 'none', resize: 'none', height: '45px', fontFamily: 'inherit' }}
|
| 197 |
/>
|
| 198 |
+
<button
|
| 199 |
+
onClick={sendMsg}
|
| 200 |
+
disabled={!text.trim()}
|
| 201 |
+
style={{
|
| 202 |
+
background: text.trim() ? '#EF4444' : 'rgba(255,255,255,0.05)',
|
| 203 |
+
color: text.trim() ? 'white' : '#64748b', border: 'none', borderRadius: '50%',
|
| 204 |
width: '45px', height: '45px', cursor: text.trim() ? 'pointer' : 'not-allowed',
|
| 205 |
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
| 206 |
transition: 'all 0.2s ease'
|
|
|
|
| 222 |
const [applicants, setApplicants] = useState({ interviews: [], accepted: [], rejected: [] });
|
| 223 |
|
| 224 |
// Modals State
|
| 225 |
+
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
|
| 226 |
const [isMessageModalOpen, setIsMessageModalOpen] = useState(false);
|
| 227 |
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
| 228 |
|
|
|
|
| 314 |
};
|
| 315 |
|
| 316 |
// 1. Fetch Data on Component Mount
|
| 317 |
+
useEffect(() => {
|
| 318 |
+
fetchData();
|
| 319 |
}, []);
|
| 320 |
|
| 321 |
// 2. Updated Schedule Handler
|
|
|
|
| 357 |
location: mode === 'Offline' ? details : null,
|
| 358 |
interviewer_name: interviewerName,
|
| 359 |
interviewer_role: interviewerRole,
|
| 360 |
+
duration_mins: 45
|
| 361 |
};
|
| 362 |
|
| 363 |
let dbError;
|
|
|
|
| 487 |
|
| 488 |
<AnimatePresence>
|
| 489 |
{isMessageModalOpen && (
|
| 490 |
+
<AdminChatModal
|
| 491 |
+
isOpen={isMessageModalOpen}
|
| 492 |
+
onClose={() => setIsMessageModalOpen(false)}
|
| 493 |
+
applicant={selectedApplicant}
|
| 494 |
/>
|
| 495 |
)}
|
| 496 |
</AnimatePresence>
|
| 497 |
+
|
| 498 |
<AnimatePresence>
|
| 499 |
{isDrawerOpen && (
|
| 500 |
+
<FullProfileOverlay
|
| 501 |
+
candidate={drawerCandidate}
|
| 502 |
+
onClose={() => setIsDrawerOpen(false)}
|
| 503 |
/>
|
| 504 |
)}
|
| 505 |
</AnimatePresence>
|
src/components/ApplicantLayout.jsx
CHANGED
|
@@ -171,6 +171,13 @@ export default function ApplicantLayout({ children, activePage, onNavigate }) {
|
|
| 171 |
async (payload) => {
|
| 172 |
console.log("=== MESSAGES PAYLOAD ===", payload);
|
| 173 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
// Fetch the sender's name from user_roles/companies
|
| 175 |
let senderName = 'Admin / HR';
|
| 176 |
const { data: roleData } = await supabase
|
|
|
|
| 171 |
async (payload) => {
|
| 172 |
console.log("=== MESSAGES PAYLOAD ===", payload);
|
| 173 |
|
| 174 |
+
// Ignore applicant's own manual replies to the system thread
|
| 175 |
+
if (payload.new.sender_id === user.id) {
|
| 176 |
+
if (!payload.new.content || !payload.new.content.startsWith("Hello, Thank you for applying")) {
|
| 177 |
+
return;
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
// Fetch the sender's name from user_roles/companies
|
| 182 |
let senderName = 'Admin / HR';
|
| 183 |
const { data: roleData } = await supabase
|
src/components/JobListings.jsx
CHANGED
|
@@ -48,15 +48,15 @@ export default function JobListings({ searchQuery, setSearchQuery, isSearching,
|
|
| 48 |
// ✅ Helper: Send welcome message to applicant
|
| 49 |
const sendApplicationConfirmationMessage = async (userId, jobTitle) => {
|
| 50 |
try {
|
| 51 |
-
const { data: { user: adminUser } } = await supabase.auth.getUser();
|
| 52 |
-
if (!adminUser) return;
|
| 53 |
-
|
| 54 |
const message = `Hello, Thank you for applying for the **${jobTitle}** position. We have received your application and our team is currently reviewing your profile. If your qualifications match our requirements, we will contact you shortly regarding the next steps in the selection process. We appreciate your interest in this opportunity.`;
|
| 55 |
|
|
|
|
|
|
|
| 56 |
const { error } = await supabase.from('messages').insert([{
|
| 57 |
-
sender_id:
|
| 58 |
receiver_id: userId,
|
| 59 |
-
content: message
|
|
|
|
| 60 |
}]);
|
| 61 |
|
| 62 |
if (error) console.error('Error sending confirmation message:', error);
|
|
|
|
| 48 |
// ✅ Helper: Send welcome message to applicant
|
| 49 |
const sendApplicationConfirmationMessage = async (userId, jobTitle) => {
|
| 50 |
try {
|
|
|
|
|
|
|
|
|
|
| 51 |
const message = `Hello, Thank you for applying for the **${jobTitle}** position. We have received your application and our team is currently reviewing your profile. If your qualifications match our requirements, we will contact you shortly regarding the next steps in the selection process. We appreciate your interest in this opportunity.`;
|
| 52 |
|
| 53 |
+
// Insert message using applicant's ID for both sender and receiver
|
| 54 |
+
// (RLS prevents applicants from inserting messages on behalf of an Admin)
|
| 55 |
const { error } = await supabase.from('messages').insert([{
|
| 56 |
+
sender_id: userId,
|
| 57 |
receiver_id: userId,
|
| 58 |
+
content: message,
|
| 59 |
+
is_read: false
|
| 60 |
}]);
|
| 61 |
|
| 62 |
if (error) console.error('Error sending confirmation message:', error);
|
src/pages/ApplicantMessages.jsx
CHANGED
|
@@ -3,22 +3,22 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|
| 3 |
import { supabase } from '../supabaseClient';
|
| 4 |
import ApplicantLayout from '../components/ApplicantLayout';
|
| 5 |
import PlaceholderContent from '../components/PlaceholderContent';
|
| 6 |
-
import { ChatIcon, SearchIcon } from '../components/Icons';
|
| 7 |
|
| 8 |
// --- Inline UI Components for Chat ---
|
| 9 |
const SendIcon = () => (
|
| 10 |
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
| 11 |
-
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
| 12 |
</svg>
|
| 13 |
);
|
| 14 |
|
| 15 |
const Avatar = ({ name }) => (
|
| 16 |
-
<div style={{
|
| 17 |
-
width: 45, height: 45, borderRadius: '50%',
|
| 18 |
-
background: 'linear-gradient(135deg, #374151, #1f2937)',
|
| 19 |
-
border: '2px solid rgba(255,255,255,0.1)',
|
| 20 |
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 21 |
-
fontWeight: 'bold', color: 'white', flexShrink: 0
|
| 22 |
}}>
|
| 23 |
{name ? name[0].toUpperCase() : 'U'}
|
| 24 |
</div>
|
|
@@ -55,17 +55,30 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 55 |
|
| 56 |
const threadMap = {};
|
| 57 |
messages.forEach(m => {
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
if (!threadMap[otherId]) {
|
| 62 |
-
threadMap[otherId] = {
|
| 63 |
-
id: otherId,
|
| 64 |
-
name: 'Admin / HR',
|
| 65 |
-
subj: 'Application Update',
|
| 66 |
-
last: '',
|
| 67 |
-
unread: false,
|
| 68 |
-
time: m.created_at,
|
| 69 |
msgs: [],
|
| 70 |
companyName: '',
|
| 71 |
companyLogo: '',
|
|
@@ -73,11 +86,12 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 73 |
};
|
| 74 |
}
|
| 75 |
|
| 76 |
-
threadMap[otherId].msgs.push({
|
| 77 |
-
id: m.id,
|
| 78 |
-
text: m.content,
|
| 79 |
-
time: new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
| 80 |
-
|
|
|
|
| 81 |
});
|
| 82 |
threadMap[otherId].last = m.content;
|
| 83 |
threadMap[otherId].time = m.created_at;
|
|
@@ -91,7 +105,7 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 91 |
if (otherUserIds.length > 0) {
|
| 92 |
const { data: rolesData } = await supabase
|
| 93 |
.from('user_roles')
|
| 94 |
-
.select('user_id, name, company_id')
|
| 95 |
.in('user_id', otherUserIds);
|
| 96 |
|
| 97 |
console.log("Roles Data:", rolesData);
|
|
@@ -106,7 +120,7 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 106 |
// ✅ Fetch Company Details including logo from storage
|
| 107 |
const companyIds = [...new Set(rolesData.map(r => r.company_id).filter(Boolean))];
|
| 108 |
console.log("Company IDs to fetch:", companyIds);
|
| 109 |
-
|
| 110 |
if (companyIds.length > 0) {
|
| 111 |
const { data: companiesData } = await supabase
|
| 112 |
.from('companies')
|
|
@@ -134,7 +148,7 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 134 |
|
| 135 |
console.log("Final Thread List:", threadList);
|
| 136 |
setThreads(threadList);
|
| 137 |
-
|
| 138 |
// Refresh currently open chat window if data changed
|
| 139 |
if (selected) {
|
| 140 |
const updated = threadList.find(t => t.id === selected.id);
|
|
@@ -158,11 +172,11 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 158 |
|
| 159 |
// Create a channel to listen for any new messages where THIS user is the receiver
|
| 160 |
channel = supabase.channel('applicant_inbox')
|
| 161 |
-
.on('postgres_changes', {
|
| 162 |
-
event: 'INSERT',
|
| 163 |
-
schema: 'public',
|
| 164 |
table: 'messages',
|
| 165 |
-
filter: `receiver_id=eq.${user.id}`
|
| 166 |
}, () => {
|
| 167 |
console.log("New message received from Admin!");
|
| 168 |
fetchMsgs(user.id);
|
|
@@ -194,11 +208,11 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 194 |
setText('');
|
| 195 |
|
| 196 |
// Send to Supabase
|
| 197 |
-
const { error } = await supabase.from('messages').insert([{
|
| 198 |
-
sender_id: userId,
|
| 199 |
-
receiver_id: selected.id,
|
| 200 |
-
content: messageText,
|
| 201 |
-
is_read: false
|
| 202 |
}]);
|
| 203 |
|
| 204 |
if (error) {
|
|
@@ -208,14 +222,48 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 208 |
}
|
| 209 |
};
|
| 210 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
const filtered = threads.filter(t => t.name.toLowerCase().includes(search.toLowerCase()));
|
| 212 |
|
| 213 |
return (
|
| 214 |
<ApplicantLayout activePage="applicant-messages" onNavigate={onNavigate}>
|
| 215 |
<style>{`.hide-scroll::-webkit-scrollbar { width: 0px; }`}</style>
|
| 216 |
-
|
| 217 |
<div style={{ display: 'flex', height: 'calc(100vh - 140px)', gap: '1.5rem', paddingBottom: '1rem' }}>
|
| 218 |
-
|
| 219 |
{/* --- CONVERSATIONS LIST --- */}
|
| 220 |
<div style={{ ...glassStyle, width: '350px', flexShrink: 0 }}>
|
| 221 |
<div style={{ padding: '1.5rem' }}>
|
|
@@ -235,36 +283,36 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 235 |
filtered.map(t => {
|
| 236 |
const isAct = selected?.id === t.id;
|
| 237 |
return (
|
| 238 |
-
<motion.div
|
| 239 |
key={t.id} onClick={() => markAsRead(t)}
|
| 240 |
-
whileHover={{ scale: 0.98 }}
|
| 241 |
-
style={{
|
| 242 |
-
padding: '1.25rem', marginBottom: '0.5rem', borderRadius: '1rem',
|
| 243 |
cursor: 'pointer', position: 'relative',
|
| 244 |
-
background: isAct ? 'rgba(251, 191, 36, 0.08)' : 'transparent',
|
| 245 |
border: isAct ? '1px solid rgba(251,191,36,0.3)' : '1px solid transparent',
|
| 246 |
display: 'flex', gap: '1rem'
|
| 247 |
}}
|
| 248 |
>
|
| 249 |
{t.unread && !isAct && <div style={{ position: 'absolute', top: '1.5rem', right: '1.25rem', width: '10px', height: '10px', backgroundColor: '#FBBF24', borderRadius: '50%' }}></div>}
|
| 250 |
-
|
| 251 |
{/* Company Logo */}
|
| 252 |
{t.companyLogo ? (
|
| 253 |
-
<img
|
| 254 |
-
src={t.companyLogo}
|
| 255 |
-
alt={t.companyName}
|
| 256 |
style={{ width: 48, height: 48, borderRadius: '0.5rem', objectFit: 'cover', flexShrink: 0, border: '1px solid rgba(255,255,255,0.2)' }}
|
| 257 |
/>
|
| 258 |
) : (
|
| 259 |
<Avatar name={t.companyName || t.name} />
|
| 260 |
)}
|
| 261 |
-
|
| 262 |
{/* Message Info */}
|
| 263 |
<div style={{ flex: 1, minWidth: 0 }}>
|
| 264 |
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
| 265 |
<span style={{ fontWeight: 'bold', color: isAct ? '#FCD34D' : 'white', fontSize: '0.95rem' }}>{t.companyName || t.name}</span>
|
| 266 |
<span style={{ background: 'rgba(34, 197, 94, 0.2)', color: '#22c55e', padding: '0.15rem 0.5rem', borderRadius: '0.25rem', fontSize: '0.65rem', fontWeight: '600', flexShrink: 0 }}>HR</span>
|
| 267 |
-
<span style={{ fontSize: '0.7rem', color: t.unread ? '#FCD34D' : '#64748b', marginLeft: 'auto', flexShrink: 0 }}>{new Date(t.time).toLocaleDateString([],{month:'short', day:'numeric'})}</span>
|
| 268 |
</div>
|
| 269 |
<div style={{ fontSize: '0.8rem', color: '#e2e8f0', margin: '0.2rem 0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.subj}</div>
|
| 270 |
<div style={{ fontSize: '0.75rem', color: '#94a3b8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.last}</div>
|
|
@@ -282,9 +330,9 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 282 |
<>
|
| 283 |
<div style={{ padding: '1.25rem 2rem', background: 'rgba(0,0,0,0.2)', borderBottom: '1px solid rgba(255,255,255,0.08)', display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
| 284 |
{selected.companyLogo ? (
|
| 285 |
-
<img
|
| 286 |
-
src={selected.companyLogo}
|
| 287 |
-
alt={selected.companyName}
|
| 288 |
style={{ width: 45, height: 45, borderRadius: '0.5rem', objectFit: 'cover', border: '2px solid rgba(255,255,255,0.2)', flexShrink: 0 }}
|
| 289 |
/>
|
| 290 |
) : (
|
|
@@ -301,16 +349,23 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 301 |
|
| 302 |
<div className="hide-scroll" style={{ flex: 1, padding: '2rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
| 303 |
{selected.msgs.map(m => (
|
| 304 |
-
<motion.div key={m.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} style={{ alignSelf: m.isMe ? 'flex-end' : 'flex-start', maxWidth: '70%' }}>
|
| 305 |
-
<div style={{
|
| 306 |
-
background: m.isMe ? 'linear-gradient(135deg, #FBBF24, #F59E0B)' : 'rgba(255,255,255,0.06)',
|
| 307 |
-
color: m.isMe ? '#020617' : 'white',
|
| 308 |
-
padding: '1rem 1.25rem', borderRadius: '1.25rem',
|
| 309 |
-
borderBottomRightRadius: m.isMe ? 4 : '1.25rem', borderBottomLeftRadius: m.isMe ? '1.25rem' : 4
|
| 310 |
}}>
|
| 311 |
{m.text}
|
| 312 |
</div>
|
| 313 |
-
<div style={{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
</motion.div>
|
| 315 |
))}
|
| 316 |
<div ref={scrollRef} />
|
|
@@ -318,12 +373,12 @@ export default function ApplicantMessages({ onNavigate }) {
|
|
| 318 |
|
| 319 |
<div style={{ padding: '1.5rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
| 320 |
<div style={{ display: 'flex', gap: '1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '1rem', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.1)' }}>
|
| 321 |
-
<textarea
|
| 322 |
-
value={text}
|
| 323 |
-
onChange={e => setText(e.target.value)}
|
| 324 |
-
onKeyDown={e => { if(e.key === 'Enter' && !e.shiftKey){ e.preventDefault(); sendMsg(); }}}
|
| 325 |
-
placeholder="Type your reply..."
|
| 326 |
-
style={{ ...inputStyle, resize: 'none', height: '45px', fontFamily: 'inherit' }}
|
| 327 |
/>
|
| 328 |
<button onClick={sendMsg} disabled={!text.trim()} style={{ background: text.trim() ? '#FBBF24' : 'rgba(255,255,255,0.05)', color: text.trim() ? '#020617' : '#64748b', border: 'none', borderRadius: '50%', width: 45, height: 45, cursor: text.trim() ? 'pointer' : 'not-allowed', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
| 329 |
<SendIcon />
|
|
|
|
| 3 |
import { supabase } from '../supabaseClient';
|
| 4 |
import ApplicantLayout from '../components/ApplicantLayout';
|
| 5 |
import PlaceholderContent from '../components/PlaceholderContent';
|
| 6 |
+
import { ChatIcon, SearchIcon } from '../components/Icons';
|
| 7 |
|
| 8 |
// --- Inline UI Components for Chat ---
|
| 9 |
const SendIcon = () => (
|
| 10 |
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
| 11 |
+
<line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" />
|
| 12 |
</svg>
|
| 13 |
);
|
| 14 |
|
| 15 |
const Avatar = ({ name }) => (
|
| 16 |
+
<div style={{
|
| 17 |
+
width: 45, height: 45, borderRadius: '50%',
|
| 18 |
+
background: 'linear-gradient(135deg, #374151, #1f2937)',
|
| 19 |
+
border: '2px solid rgba(255,255,255,0.1)',
|
| 20 |
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
| 21 |
+
fontWeight: 'bold', color: 'white', flexShrink: 0
|
| 22 |
}}>
|
| 23 |
{name ? name[0].toUpperCase() : 'U'}
|
| 24 |
</div>
|
|
|
|
| 55 |
|
| 56 |
const threadMap = {};
|
| 57 |
messages.forEach(m => {
|
| 58 |
+
let isMe = m.sender_id === uid;
|
| 59 |
+
let otherId = isMe ? m.receiver_id : m.sender_id;
|
| 60 |
+
|
| 61 |
+
// --- FIX FOR AUTOMATED MESSAGES ---
|
| 62 |
+
// If the applicant sent it to themselves, it's a thread used for system messages.
|
| 63 |
+
if (m.sender_id === m.receiver_id && m.sender_id === uid) {
|
| 64 |
+
otherId = uid; // Keep a valid UUID so replies work
|
| 65 |
+
|
| 66 |
+
// Only force the automated welcome message to the left side
|
| 67 |
+
if (m.content && m.content.startsWith("Hello, Thank you for applying")) {
|
| 68 |
+
isMe = false;
|
| 69 |
+
} else {
|
| 70 |
+
isMe = true; // Applicant's manual replies stay on the right
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
|
| 74 |
if (!threadMap[otherId]) {
|
| 75 |
+
threadMap[otherId] = {
|
| 76 |
+
id: otherId,
|
| 77 |
+
name: 'Admin / HR',
|
| 78 |
+
subj: 'Application Update',
|
| 79 |
+
last: '',
|
| 80 |
+
unread: false,
|
| 81 |
+
time: m.created_at,
|
| 82 |
msgs: [],
|
| 83 |
companyName: '',
|
| 84 |
companyLogo: '',
|
|
|
|
| 86 |
};
|
| 87 |
}
|
| 88 |
|
| 89 |
+
threadMap[otherId].msgs.push({
|
| 90 |
+
id: m.id,
|
| 91 |
+
text: m.content,
|
| 92 |
+
time: new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
| 93 |
+
rawTime: m.created_at,
|
| 94 |
+
isMe
|
| 95 |
});
|
| 96 |
threadMap[otherId].last = m.content;
|
| 97 |
threadMap[otherId].time = m.created_at;
|
|
|
|
| 105 |
if (otherUserIds.length > 0) {
|
| 106 |
const { data: rolesData } = await supabase
|
| 107 |
.from('user_roles')
|
| 108 |
+
.select('user_id, name, company_id')
|
| 109 |
.in('user_id', otherUserIds);
|
| 110 |
|
| 111 |
console.log("Roles Data:", rolesData);
|
|
|
|
| 120 |
// ✅ Fetch Company Details including logo from storage
|
| 121 |
const companyIds = [...new Set(rolesData.map(r => r.company_id).filter(Boolean))];
|
| 122 |
console.log("Company IDs to fetch:", companyIds);
|
| 123 |
+
|
| 124 |
if (companyIds.length > 0) {
|
| 125 |
const { data: companiesData } = await supabase
|
| 126 |
.from('companies')
|
|
|
|
| 148 |
|
| 149 |
console.log("Final Thread List:", threadList);
|
| 150 |
setThreads(threadList);
|
| 151 |
+
|
| 152 |
// Refresh currently open chat window if data changed
|
| 153 |
if (selected) {
|
| 154 |
const updated = threadList.find(t => t.id === selected.id);
|
|
|
|
| 172 |
|
| 173 |
// Create a channel to listen for any new messages where THIS user is the receiver
|
| 174 |
channel = supabase.channel('applicant_inbox')
|
| 175 |
+
.on('postgres_changes', {
|
| 176 |
+
event: 'INSERT',
|
| 177 |
+
schema: 'public',
|
| 178 |
table: 'messages',
|
| 179 |
+
filter: `receiver_id=eq.${user.id}`
|
| 180 |
}, () => {
|
| 181 |
console.log("New message received from Admin!");
|
| 182 |
fetchMsgs(user.id);
|
|
|
|
| 208 |
setText('');
|
| 209 |
|
| 210 |
// Send to Supabase
|
| 211 |
+
const { error } = await supabase.from('messages').insert([{
|
| 212 |
+
sender_id: userId,
|
| 213 |
+
receiver_id: selected.id,
|
| 214 |
+
content: messageText,
|
| 215 |
+
is_read: false
|
| 216 |
}]);
|
| 217 |
|
| 218 |
if (error) {
|
|
|
|
| 222 |
}
|
| 223 |
};
|
| 224 |
|
| 225 |
+
const unsendMsg = async (msgId) => {
|
| 226 |
+
if (!window.confirm("Unsend this message?")) return;
|
| 227 |
+
|
| 228 |
+
// 1. Try to delete the message
|
| 229 |
+
const { data, error } = await supabase.from('messages').delete().eq('id', msgId).select();
|
| 230 |
+
|
| 231 |
+
if (error) {
|
| 232 |
+
console.error("Unsend Error:", error);
|
| 233 |
+
alert("Failed to unsend message.");
|
| 234 |
+
return;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// 2. If RLS silently blocks deletion (data is empty), fallback to updating the content
|
| 238 |
+
if (!data || data.length === 0) {
|
| 239 |
+
const { error: updateError } = await supabase
|
| 240 |
+
.from('messages')
|
| 241 |
+
.update({ content: "🚫 This message was unsent" })
|
| 242 |
+
.eq('id', msgId);
|
| 243 |
+
|
| 244 |
+
if (updateError) {
|
| 245 |
+
alert("Database policies prevent unsending this message.");
|
| 246 |
+
return;
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
fetchMsgs(userId);
|
| 251 |
+
};
|
| 252 |
+
|
| 253 |
+
const canUnsend = (rawTime) => {
|
| 254 |
+
if (!rawTime) return false;
|
| 255 |
+
const msgTime = new Date(rawTime).getTime();
|
| 256 |
+
return (Date.now() - msgTime) < 5 * 60 * 1000; // 5 minutes
|
| 257 |
+
};
|
| 258 |
+
|
| 259 |
const filtered = threads.filter(t => t.name.toLowerCase().includes(search.toLowerCase()));
|
| 260 |
|
| 261 |
return (
|
| 262 |
<ApplicantLayout activePage="applicant-messages" onNavigate={onNavigate}>
|
| 263 |
<style>{`.hide-scroll::-webkit-scrollbar { width: 0px; }`}</style>
|
| 264 |
+
|
| 265 |
<div style={{ display: 'flex', height: 'calc(100vh - 140px)', gap: '1.5rem', paddingBottom: '1rem' }}>
|
| 266 |
+
|
| 267 |
{/* --- CONVERSATIONS LIST --- */}
|
| 268 |
<div style={{ ...glassStyle, width: '350px', flexShrink: 0 }}>
|
| 269 |
<div style={{ padding: '1.5rem' }}>
|
|
|
|
| 283 |
filtered.map(t => {
|
| 284 |
const isAct = selected?.id === t.id;
|
| 285 |
return (
|
| 286 |
+
<motion.div
|
| 287 |
key={t.id} onClick={() => markAsRead(t)}
|
| 288 |
+
whileHover={{ scale: 0.98 }}
|
| 289 |
+
style={{
|
| 290 |
+
padding: '1.25rem', marginBottom: '0.5rem', borderRadius: '1rem',
|
| 291 |
cursor: 'pointer', position: 'relative',
|
| 292 |
+
background: isAct ? 'rgba(251, 191, 36, 0.08)' : 'transparent',
|
| 293 |
border: isAct ? '1px solid rgba(251,191,36,0.3)' : '1px solid transparent',
|
| 294 |
display: 'flex', gap: '1rem'
|
| 295 |
}}
|
| 296 |
>
|
| 297 |
{t.unread && !isAct && <div style={{ position: 'absolute', top: '1.5rem', right: '1.25rem', width: '10px', height: '10px', backgroundColor: '#FBBF24', borderRadius: '50%' }}></div>}
|
| 298 |
+
|
| 299 |
{/* Company Logo */}
|
| 300 |
{t.companyLogo ? (
|
| 301 |
+
<img
|
| 302 |
+
src={t.companyLogo}
|
| 303 |
+
alt={t.companyName}
|
| 304 |
style={{ width: 48, height: 48, borderRadius: '0.5rem', objectFit: 'cover', flexShrink: 0, border: '1px solid rgba(255,255,255,0.2)' }}
|
| 305 |
/>
|
| 306 |
) : (
|
| 307 |
<Avatar name={t.companyName || t.name} />
|
| 308 |
)}
|
| 309 |
+
|
| 310 |
{/* Message Info */}
|
| 311 |
<div style={{ flex: 1, minWidth: 0 }}>
|
| 312 |
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
| 313 |
<span style={{ fontWeight: 'bold', color: isAct ? '#FCD34D' : 'white', fontSize: '0.95rem' }}>{t.companyName || t.name}</span>
|
| 314 |
<span style={{ background: 'rgba(34, 197, 94, 0.2)', color: '#22c55e', padding: '0.15rem 0.5rem', borderRadius: '0.25rem', fontSize: '0.65rem', fontWeight: '600', flexShrink: 0 }}>HR</span>
|
| 315 |
+
<span style={{ fontSize: '0.7rem', color: t.unread ? '#FCD34D' : '#64748b', marginLeft: 'auto', flexShrink: 0 }}>{new Date(t.time).toLocaleDateString([], { month: 'short', day: 'numeric' })}</span>
|
| 316 |
</div>
|
| 317 |
<div style={{ fontSize: '0.8rem', color: '#e2e8f0', margin: '0.2rem 0', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.subj}</div>
|
| 318 |
<div style={{ fontSize: '0.75rem', color: '#94a3b8', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t.last}</div>
|
|
|
|
| 330 |
<>
|
| 331 |
<div style={{ padding: '1.25rem 2rem', background: 'rgba(0,0,0,0.2)', borderBottom: '1px solid rgba(255,255,255,0.08)', display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
| 332 |
{selected.companyLogo ? (
|
| 333 |
+
<img
|
| 334 |
+
src={selected.companyLogo}
|
| 335 |
+
alt={selected.companyName}
|
| 336 |
style={{ width: 45, height: 45, borderRadius: '0.5rem', objectFit: 'cover', border: '2px solid rgba(255,255,255,0.2)', flexShrink: 0 }}
|
| 337 |
/>
|
| 338 |
) : (
|
|
|
|
| 349 |
|
| 350 |
<div className="hide-scroll" style={{ flex: 1, padding: '2rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
| 351 |
{selected.msgs.map(m => (
|
| 352 |
+
<motion.div key={m.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} style={{ alignSelf: m.isMe ? 'flex-end' : 'flex-start', maxWidth: '70%', position: 'relative' }}>
|
| 353 |
+
<div style={{
|
| 354 |
+
background: m.isMe ? 'linear-gradient(135deg, #FBBF24, #F59E0B)' : 'rgba(255,255,255,0.06)',
|
| 355 |
+
color: m.isMe ? '#020617' : 'white',
|
| 356 |
+
padding: '1rem 1.25rem', borderRadius: '1.25rem',
|
| 357 |
+
borderBottomRightRadius: m.isMe ? 4 : '1.25rem', borderBottomLeftRadius: m.isMe ? '1.25rem' : 4
|
| 358 |
}}>
|
| 359 |
{m.text}
|
| 360 |
</div>
|
| 361 |
+
<div style={{ display: 'flex', justifyContent: m.isMe ? 'flex-end' : 'flex-start', alignItems: 'center', gap: '8px', marginTop: '4px' }}>
|
| 362 |
+
<div style={{ fontSize: '0.7rem', color: '#64748b' }}>{m.time}</div>
|
| 363 |
+
{m.isMe && canUnsend(m.rawTime) && (
|
| 364 |
+
<button onClick={() => unsendMsg(m.id)} title="Unsend message (5 min window)" style={{ background: 'transparent', border: 'none', color: '#ef4444', cursor: 'pointer', padding: '0 2px', display: 'flex', alignItems: 'center' }}>
|
| 365 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
|
| 366 |
+
</button>
|
| 367 |
+
)}
|
| 368 |
+
</div>
|
| 369 |
</motion.div>
|
| 370 |
))}
|
| 371 |
<div ref={scrollRef} />
|
|
|
|
| 373 |
|
| 374 |
<div style={{ padding: '1.5rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
| 375 |
<div style={{ display: 'flex', gap: '1rem', background: 'rgba(255,255,255,0.03)', borderRadius: '1rem', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.1)' }}>
|
| 376 |
+
<textarea
|
| 377 |
+
value={text}
|
| 378 |
+
onChange={e => setText(e.target.value)}
|
| 379 |
+
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); } }}
|
| 380 |
+
placeholder="Type your reply..."
|
| 381 |
+
style={{ ...inputStyle, resize: 'none', height: '45px', fontFamily: 'inherit' }}
|
| 382 |
/>
|
| 383 |
<button onClick={sendMsg} disabled={!text.trim()} style={{ background: text.trim() ? '#FBBF24' : 'rgba(255,255,255,0.05)', color: text.trim() ? '#020617' : '#64748b', border: 'none', borderRadius: '50%', width: 45, height: 45, cursor: text.trim() ? 'pointer' : 'not-allowed', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
| 384 |
<SendIcon />
|