main
LingoRoom — Translate Chat * { margin:0; padding:0; box-sizing:border-box; font-family:'Segoe UI',system-ui,sans-serif; } body { min-height:100vh; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); display:flex; align-items:center; justify-content:center; padding:16px; } .card { background:#fff; border-radius:20px; box-shadow:0 20px 60px rgba(0,0,0,.3); width:100%; max-width:420px; padding:32px 28px; animation:pop .35s ease; } @keyframes pop { from{transform:scale(.92);opacity:0} to{transform:scale(1);opacity:1} } h1 { font-size:26px; color:#333; text-align:center; margin-bottom:4px; } .sub { text-align:center; color:#888; font-size:14px; margin-bottom:22px; } .tabs { display:flex; background:#f1f1f5; border-radius:12px; padding:4px; margin-bottom:20px; } .tabs button { flex:1; border:none; background:transparent; padding:10px; border-radius:9px; cursor:pointer; font-weight:600; color:#777; } .tabs button.active { background:#fff; color:#667eea; box-shadow:0 2px 8px rgba(0,0,0,.08); } input, select { width:100%; padding:13px 15px; border:2px solid #e5e5ef; border-radius:12px; font-size:15px; margin-bottom:14px; outline:none; transition:.2s; } input:focus { border-color:#667eea; } .btn { width:100%; padding:14px; border:none; border-radius:12px; background:linear-gradient(135deg,#667eea,#764ba2); color:#fff; font-size:16px; font-weight:700; cursor:pointer; transition:.2s; } .btn:hover { transform:translateY(-2px); box-shadow:0 8px 20px rgba(102,126,234,.4); } .btn.secondary { background:#f1f1f5; color:#555; margin-top:10px; } .error { color:#e74c3c; font-size:13px; text-align:center; margin-bottom:12px; min-height:16px; } .hidden { display:none !important; } /* Dashboard */ .dash-btn { display:flex; align-items:center; gap:14px; width:100%; padding:18px; border:2px solid #ececf4; border-radius:14px; background:#fafaff; cursor:pointer; margin-bottom:14px; text-align:left; transition:.2s; } .dash-btn:hover { border-color:#667eea; background:#f4f4ff; } .dash-btn .ico { font-size:26px; } .dash-btn b { color:#333; font-size:16px; display:block; } .dash-btn span { color:#888; font-size:13px; } .logout { text-align:center; color:#aaa; font-size:13px; cursor:pointer; margin-top:8px; } /* Code screen */ .code-box { text-align:center; } .code { font-size:44px; letter-spacing:12px; font-weight:800; color:#667eea; background:#f4f4ff; border-radius:14px; padding:16px 8px 16px 20px; margin:18px 0; user-select:all; } .copy-btn { background:#f1f1f5; border:none; padding:10px 22px; border-radius:10px; cursor:pointer; font-weight:600; color:#555; } /* Language pick */ .lang-grid { display:grid; grid-template-columns:1fr 1fr; gap:14px; } .lang-opt { border:2px solid #ececf4; border-radius:14px; padding:24px 10px; text-align:center; cursor:pointer; transition:.2s; } .lang-opt:hover, .lang-opt.sel { border-color:#667eea; background:#f4f4ff; } .lang-opt .flag { font-size:38px; } .lang-opt b { display:block; margin-top:8px; color:#333; } /* Chat */ .chat-card { max-width:560px; padding:0; overflow:hidden; display:flex; flex-direction:column; height:80vh; min-height:480px; } .chat-head { background:linear-gradient(135deg,#667eea,#764ba2); color:#fff; padding:14px 18px; display:flex; align-items:center; gap:12px; } .chat-head .ava { width:40px; height:40px; border-radius:50%; background:rgba(255,255,255,.25); display:flex; align-items:center; justify-content:center; font-size:20px; } .chat-head b { font-size:16px; } .chat-head small { opacity:.8; display:block; font-size:12px; } .chat-head .leave { margin-left:auto; background:rgba(255,255,255,.2); border:none; color:#fff; padding:8px 14px; border-radius:10px; cursor:pointer; font-size:13px; } .msgs { flex:1; overflow-y:auto; padding:18px; background:#f7f7fb; display:flex; flex-direction:column; gap:10px; } .msg { max-width:75%; padding:10px 14px; border-radius:16px; font-size:15px; line-height:1.4; animation:pop .2s ease; word-wrap:break-word; } .msg.me { align-self:flex-end; background:linear-gradient(135deg,#667eea,#764ba2); color:#fff; border-bottom-right-radius:4px; } .msg.them { align-self:flex-start; background:#fff; color:#333; border:1px solid #ececf4; border-bottom-left-radius:4px; box-shadow:0 2px 6px rgba(0,0,0,.05); } .msg .meta { font-size:11px; opacity:.65; margin-top:4px; } .msg .translated-tag { font-size:10px; opacity:.6; font-style:italic; } .input-bar { display:flex; gap:8px; padding:12px; background:#fff; border-top:1px solid #eee; align-items:center; } .input-bar input { margin:0; flex:1; } .icon-btn { border:none; background:#f1f1f5; width:44px; height:44px; border-radius:12px; font-size:20px; cursor:pointer; flex-shrink:0; } .icon-btn.send { background:linear-gradient(135deg,#667eea,#764ba2); color:#fff; } .emoji-panel { position:absolute; bottom:70px; left:12px; right:12px; background:#fff; border-radius:14px; box-shadow:0 -4px 20px rgba(0,0,0,.15); padding:12px; display:grid; grid-template-columns:repeat(8,1fr); gap:4px; z-index:10; } .emoji-panel button { border:none; background:none; font-size:22px; cursor:pointer; padding:6px; border-radius:8px; } .emoji-panel button:hover { background:#f1f1f5; } .chat-wrap { position:relative; width:100%; max-width:560px; } .typing { font-size:12px; color:#999; padding:0 18px 6px; background:#f7f7fb; font-style:italic; height:20px; }

💬 LingoRoom

Chat across languages, instantly

/* ================= STORAGE & STATE ================= */ const LS = { users:'lr_users', session:'lr_session', rooms:'lr_rooms', lang:'lr_lang_' }; let state = { user:null, room:null, lang:null, mode:null }; const channel = new BroadcastChannel('lingoroom'); const $ = id => document.getElementById(id); const store = { get:(k,d)=>{ try{return JSON.parse(localStorage.getItem(k))??d}catch(e){return d} }, set:(k,v)=>localStorage.setItem(k,JSON.stringify(v)) }; const views = ['view-login','view-dash','view-code','view-lang','view-chat']; function show(id){ views.forEach(v=>$(v).classList.add('hidden')); $(id).classList.remove('hidden'); } /* ================= AUTH ================= */ let authMode='login'; function switchTab(m){ authMode=m; $('tab-login').classList.toggle('active',m==='login'); $('tab-signup').classList.toggle('active',m==='signup'); $('auth-name').classList.toggle('hidden',m==='login'); $('auth-btn').textContent = m==='login'?'Login':'Create Account'; $('auth-error').textContent=''; } function handleAuth(){ const email=$('auth-email').value.trim().toLowerCase(), pass=$('auth-pass').value, name=$('auth-name').value.trim(); const users=store.get(LS.users,{}); if(authMode==='signup'){ if(!name||!email||!pass) return $('auth-error').textContent='Fill in all fields'; if(users[email]) return $('auth-error').textContent='Account already exists — please login'; users[email]={name,pass}; store.set(LS.users,users); } else { if(!users[email]||users[email].pass!==pass) return $('auth-error').textContent='Invalid email or password'; } store.set(LS.session,{email,name:name||users[email].name}); enterDash(); } function logout(){ localStorage.removeItem(LS.session); location.reload(); } function enterDash(){ state.user=store.get(LS.session,null); if(!state.user){ show('view-login'); return; } $('dash-name').textContent=state.user.name; show('view-dash'); } /* ================= ROOMS ================= */ function createRoom(){ const code=String(Math.floor(100000+Math.random()*900000)); const rooms=store.get(LS.rooms,{}); rooms[code]={createdBy:state.user.email,createdAt:Date.now()}; store.set(LS.rooms,rooms); state.room=code; state.mode='create'; $('room-code').textContent=code; show('view-code'); } function copyCode(){ navigator.clipboard.writeText(state.room).then(()=>{ event.target.textContent='✅ Copied!'; setTimeout(()=>event.target.textContent='📋 Copy Code',1500); }); } function showJoin(){ $('join-area').classList.toggle('hidden'); $('join-error').textContent=''; } function joinRoom(){ const code=$('join-code').value.trim(); const rooms=store.get(LS.rooms,{}); if(!/^\d{6}$/.test(code)) return $('join-error').textContent='Code must be 6 digits'; if(!rooms[code]) return $('join-error').textContent='Room not found. Check the code!'; state.room=code; state.mode='join'; enterLanguage(); } function enterLanguage(){ show('view-lang'); } function pickLang(lang){ state.lang=lang; localStorage.setItem(LS.lang+state.room,lang); show('view-chat'); $('chat-code-label').textContent='#'+state.room; $('chat-lang-label').textContent='You are chatting in '+(lang==='ru'?'🇷🇺 Russian':'🇬🇧 English'); loadHistory(); channel.postMessage({type:'join',room:state.room,user:state.user.name}); requestNotifyPermission(); } /* ================= TRANSLATION ================= */ const fallbackDict={ 'hello':'привет','hi':'привет','how are you':'как дела','how are you?':'как дела?','good morning':'доброе утро', 'good night':'спокойной ночи','thank you':'спасибо','thanks':'спасибо','yes':'да','no':'нет','friend':'друг', 'what is your name':'как тебя зовут','my name is':'меня зовут','i love you':'я люблю тебя','goodbye':'до свидания','bye':'пока', 'water':'вода','food':'еда','today':'сегодня','tomorrow':'завтра','help':'помощь','ok':'хорошо','see you':'увидимся', 'where are you':'где ты','i am fine':'я в порядке','welcome':'добро пожаловать','cat':'кот','dog':'собака','house':'дом' }; async function translateText(text,from,to){ if(from===to) return text; try{ const r=await fetch(`https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${from}|${to}`); const d=await r.json(); const t=d?.responseData?.translatedText; if(t && !d.responseStatus?.toString().startsWith('4')) return t; }catch(e){} // fallback: word-by-word dictionary return text.split(/\b/).map(w=>{ const lw=w.toLowerCase(); if(fallbackDict[lw]) return (w[0]===w[0]?.toUpperCase()? fallbackDict[lw][0].toUpperCase()+fallbackDict[lw].slice(1):fallbackDict[lw]); return w; }).join(''); } /* ================= CHAT ================= */ const EMOJIS='😀😁😂🤣😊😍😘😎🤔😅😉🙃😢😭😡👍👎🙏👏💪🔥❤️💯✨🎉🌍🤝💬'.split(/(?=\uD83D|\uD83C|\uD83E)/u).filter(Boolean); $('emoji-panel').innerHTML=EMOJIS.map(e=>``).join(''); function toggleEmoji(){ $('emoji-panel').classList.toggle('hidden'); } function addEmoji(e){ $('msg-input').value+=e; $('emoji-panel').classList.add('hidden'); $('msg-input').focus(); } function esc(s){ const d=document.createElement('div'); d.textContent=s; return d.innerHTML; } async function sendMsg(){ const inp=$('msg-input'), text=inp.value.trim(); if(!text||!state.room) return; inp.value=''; const msg={type:'msg',room:state.room,from:state.user.name,email:state.user.email,text,lang:state.lang,ts:Date.now()}; renderMsg(msg,true); saveMsg(msg); channel.postMessage(msg); } async function renderMsg(m,mine){ const box=$('msgs'); const div=document.createElement('div'); div.className='msg '+(mine?'me':'them'); let html=esc(m.text); let tag=''; if(!mine && m.lang!==state.lang){ tag='
⏳ translating…
'; } div.innerHTML=`${html}${tag}
${esc(m.from)} · ${new Date(m.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}
`; box.appendChild(div); box.scrollTop=box.scrollHeight; if(!mine && m.lang!==state.lang){ const t=await translateText(m.text,m.lang,state.lang); tag=`
🔁 translated ${m.lang==='ru'?'🇷🇺':'🇬🇧'} → ${state.lang==='ru'?'🇷🇺':'🇬🇧'}
`; // show original + translation div.innerHTML=`${html}
${esc(t)}
${tag}
${esc(m.from)} · ${new Date(m.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}
`; box.scrollTop=box.scrollHeight; } } function historyKey(){ return 'lr_hist_'+state.room; } function saveMsg(m){ const h=store.get(historyKey(),[]); h.push(m); store.set(historyKey(),h); } function loadHistory(){ $('msgs').innerHTML=''; store.get(historyKey(),[]).forEach(m=>renderMsg(m,m.email===state.user.email)); } /* ================= REALTIME (BroadcastChannel across tabs) ================= */ channel.onmessage=async e=>{ const d=e.data; if(d.room!==state.room) return; if(d.type==='msg' && d.email!==state.user.email){ renderMsg(d,false); saveMsg(d); maybeNotify(d); } if(d.type==='join' && d.user!==state.user.name){ $('typing').textContent=`${d.user} joined the room 👋`; setTimeout(()=>$('typing').textContent='',3000); } }; function leaveRoom(){ state.room=null; state.lang=null; show('view-dash'); } /* ================= PUSH NOTIFICATIONS ================= */ function requestNotifyPermission(){ if('Notification' in window && Notification.permission==='default') Notification.requestPermission(); } function maybeNotify(m){ if(document.hidden && 'Notification' in window && Notification.permission==='granted'){ new Notification(`💬 ${m.from}`,{body:m.text}); } } /* ================= INIT ================= */ enterDash();