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
👋
Start a translated conversation
Log out
🎉 Room Created!
Share this code with your friend
------
🌐 Choose Your Language
Your messages stay in your language — theirs get auto-translated
/* ================= 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();