Skip to content

Commit ca3428a

Browse files
authored
Create index.html
1 parent 7cabd28 commit ca3428a

File tree

1 file changed

+374
-0
lines changed

1 file changed

+374
-0
lines changed

app/index.html

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
<!doctype html>
2+
<html lang="es">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width,initial-scale=1" />
6+
<title>Demo Chat — Marketplace</title>
7+
<style>
8+
:root{--bg:#0b0c10;--panel:#0f1114;--muted:#9aa4b2;--accent:#ffb300;--txt:#e7eef6}
9+
body{margin:0;background:linear-gradient(180deg,#07080a, #0b0c10);color:var(--txt);font-family:Inter,system-ui,Arial,sans-serif}
10+
.app{display:grid;grid-template-columns:320px 1fr;gap:18px;padding:18px;height:100vh;box-sizing:border-box}
11+
.panel{background:var(--panel);border-radius:12px;padding:12px;box-shadow:0 6px 18px rgba(0,0,0,.6);overflow:auto}
12+
h1{margin:6px 0 12px;font-size:18px}
13+
.small{color:var(--muted);font-size:13px}
14+
label{display:block;margin:8px 0 6px;font-size:13px;color:var(--muted)}
15+
input, button, select, textarea{width:100%;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:var(--txt);box-sizing:border-box}
16+
.contacts{display:flex;flex-direction:column;gap:8px}
17+
.contact{padding:8px;border-radius:8px;background:rgba(255,255,255,0.02);cursor:pointer;display:flex;justify-content:space-between;align-items:center}
18+
.contact.selected{outline:2px solid rgba(255,179,0,.12)}
19+
#messages{height:calc(100vh - 220px);overflow:auto;padding:10px;display:flex;flex-direction:column;gap:8px}
20+
.msg{max-width:70%;padding:8px;border-radius:10px;background:rgba(255,255,255,0.03);font-size:14px}
21+
.msg.me{align-self:flex-end;background:rgba(255,179,0,0.12)}
22+
.msg .meta{font-size:12px;color:var(--muted);margin-bottom:6px}
23+
.sendRow{display:flex;gap:8px;margin-top:8px}
24+
.tiny{font-size:12px;color:var(--muted)}
25+
footer{margin-top:12px;color:var(--muted);font-size:12px;text-align:center}
26+
.btn{background:linear-gradient(90deg,#ffb300,#ff8a00);color:#0b0c10;font-weight:700;border:none}
27+
.row{display:flex;gap:8px}
28+
.flex{flex:1}
29+
.topbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
30+
.hint{font-size:12px;color:var(--muted)}
31+
</style>
32+
</head>
33+
<body>
34+
<div class="app">
35+
<!-- LEFT: contacts, config, auth -->
36+
<div class="panel">
37+
<div class="topbar">
38+
<div>
39+
<h1>Admin / Demo Chat</h1>
40+
<div class="small">Backend demo: <span id="backendUrlLabel"></span></div>
41+
</div>
42+
<div class="hint">Polling / Realtime hybrid</div>
43+
</div>
44+
45+
<div id="authArea">
46+
<label>Sesion</label>
47+
<div id="sessionInfo" class="small">No logueado (modo deviceId).</div>
48+
<div style="margin-top:8px;display:grid;grid-template-columns:1fr 1fr;gap:8px">
49+
<button id="loginGoogle" class="btn">Login Google</button>
50+
<button id="loginGithub" class="btn">Login GitHub</button>
51+
</div>
52+
<button id="logoutBtn" style="margin-top:8px">Logout</button>
53+
<div style="margin-top:10px"><small class="tiny">Si no configuras Supabase, el chat funcionará con IDs generados por dispositivo (sin auth).</small></div>
54+
</div>
55+
56+
<hr style="border:none;height:12px;opacity:.05" />
57+
58+
<label>Tu device / usuario</label>
59+
<div id="myId" style="font-weight:700;word-break:break-all"></div>
60+
<div class="tiny" style="margin-top:6px">Copia este ID en otro dispositivo para chatear contigo mismo.</div>
61+
62+
<label style="margin-top:12px">Agregar contacto (device id)</label>
63+
<div style="display:flex;gap:8px">
64+
<input id="newContact" placeholder="pega aqui el id (uuid / device)"/>
65+
<button id="addContactBtn">Agregar</button>
66+
</div>
67+
68+
<div style="margin-top:12px">
69+
<label>Contactos</label>
70+
<div class="contacts" id="contactsList"></div>
71+
</div>
72+
73+
<hr style="border:none;height:12px;opacity:.05" />
74+
75+
<label>Configuración</label>
76+
<div class="small">
77+
Polling interval (ms)
78+
<input id="pollInterval" type="number" value="1000" />
79+
<label style="margin-top:6px">BACKEND API base</label>
80+
<input id="backendUrl" placeholder="https://market-socket-back.vercel.app/back" />
81+
<div class="tiny" style="margin-top:8px">Si usas Supabase coloca SUPABASE_URL y SUPABASE_ANON_KEY abajo en el código.</div>
82+
</div>
83+
84+
<hr style="border:none;height:12px;opacity:.05" />
85+
<div class="small">
86+
<strong>Notas rápidas</strong>
87+
<ul style="padding-left:16px">
88+
<li>Vercel serverless no mantiene WS persistente. Usa polling o Supabase Realtime.</li>
89+
<li>Si usas el mismo proyecto Supabase, la sesión se mantiene entre sitios (mismo dominio de Supabase).</li>
90+
<li>Yo no guardo datos: todo queda en tu Supabase / backend.</li>
91+
</ul>
92+
</div>
93+
</div>
94+
95+
<!-- RIGHT: chat area -->
96+
<div class="panel">
97+
<div style="display:flex;justify-content:space-between;align-items:center">
98+
<h1 id="chatTitle">Chat</h1>
99+
<div class="small">Estado: <span id="status">idle</span></div>
100+
</div>
101+
102+
<div id="messages"></div>
103+
104+
<div class="sendRow">
105+
<input id="msgInput" placeholder="Escribe tu mensaje..." />
106+
<button id="sendBtn" class="btn">Enviar</button>
107+
</div>
108+
109+
<footer>
110+
<div class="small">Demo chat — polling + opcional Supabase Realtime</div>
111+
</footer>
112+
</div>
113+
</div>
114+
115+
<script>
116+
/*
117+
INSTRUCCIONES RÁPIDAS:
118+
- Pega este index.html en tu carpeta de front (ej: /back/index.html).
119+
- Opcional: llena SUPABASE_URL y SUPABASE_ANON_KEY si quieres usar Supabase Auth/Realtime.
120+
- Ajusta BACKEND_API si tu endpoint de mensajes es distinto.
121+
- Este archivo usa polling por defecto (seguro en Vercel). Si tu Supabase tiene Realtime y configuras las keys,
122+
intentará suscribirse a la tabla 'messages' (requiere permiso en Supabase).
123+
*/
124+
125+
/* ---------- CONFIG (rellena si tienes Supabase) ---------- */
126+
const BACKEND_API = "https://market-socket-back.vercel.app/back"; // <- Cambia si tu ruta es otra
127+
const SUPABASE_URL = ""; // ej: "https://xyzcompany.supabase.co"
128+
const SUPABASE_ANON_KEY = ""; // tu anon key
129+
/* --------------------------------------------------------- */
130+
131+
document.getElementById('backendUrl').value = BACKEND_API;
132+
document.getElementById('backendUrlLabel').textContent = BACKEND_API;
133+
134+
let pollIntervalMs = Number(localStorage.getItem('pollInterval') || 1000);
135+
document.getElementById('pollInterval').value = pollIntervalMs;
136+
137+
let supabase = null;
138+
let sessionUser = null;
139+
140+
// Init device id (fallback user)
141+
function getOrCreateDeviceId(){
142+
const storageKey = 'demo_device_id_v1';
143+
let id = localStorage.getItem(storageKey);
144+
if(!id){
145+
id = 'dev-' + crypto.randomUUID();
146+
localStorage.setItem(storageKey, id);
147+
}
148+
return id;
149+
}
150+
let deviceId = getOrCreateDeviceId();
151+
152+
function setMyIdDisplay(){
153+
const el = document.getElementById('myId');
154+
el.textContent = sessionUser ? sessionUser.email + ' • ' + sessionUser.id : deviceId;
155+
document.getElementById('sessionInfo').textContent = sessionUser ? `Logueado: ${sessionUser.email} (Supabase)` : 'Modo anon / deviceId';
156+
}
157+
setMyIdDisplay();
158+
159+
/* Contacts (localStorage) */
160+
function loadContacts(){
161+
return JSON.parse(localStorage.getItem('demo_contacts_v1')||'[]');
162+
}
163+
function saveContacts(list){
164+
localStorage.setItem('demo_contacts_v1', JSON.stringify(list));
165+
}
166+
function renderContacts(){
167+
const list = loadContacts();
168+
const container = document.getElementById('contactsList');
169+
container.innerHTML = '';
170+
list.forEach(id => {
171+
const div = document.createElement('div');
172+
div.className = 'contact' + (id === getActiveChatId() ? ' selected' : '');
173+
div.innerHTML = `<div style="font-size:13px;word-break:break-all">${id}</div>
174+
<div style="display:flex;gap:8px">
175+
<button data-id="${id}" class="openBtn">Abrir</button>
176+
<button data-id="${id}" class="delBtn">X</button>
177+
</div>`;
178+
container.appendChild(div);
179+
});
180+
// Attach events
181+
container.querySelectorAll('.openBtn').forEach(b=>b.onclick = e=>{ setActiveChatId(e.target.dataset.id); renderContacts(); });
182+
container.querySelectorAll('.delBtn').forEach(b=>b.onclick = e=>{ const id=e.target.dataset.id; const arr=loadContacts().filter(x=>x!==id); saveContacts(arr); renderContacts(); });
183+
}
184+
renderContacts();
185+
186+
document.getElementById('addContactBtn').onclick = () => {
187+
const v = document.getElementById('newContact').value.trim();
188+
if(!v) return alert('Pega un id válido');
189+
const arr = loadContacts();
190+
if(!arr.includes(v)) { arr.unshift(v); saveContacts(arr); renderContacts(); document.getElementById('newContact').value=''; setActiveChatId(v); }
191+
};
192+
193+
function getActiveChatId(){
194+
return localStorage.getItem('demo_active_chat') || loadContacts()[0] || null;
195+
}
196+
function setActiveChatId(id){
197+
if(!id) return;
198+
localStorage.setItem('demo_active_chat', id);
199+
document.getElementById('chatTitle').textContent = `Chat con: ${id}`;
200+
loadMessagesNow();
201+
}
202+
203+
/* Messages rendering */
204+
function renderMessages(messages){
205+
const container = document.getElementById('messages');
206+
container.innerHTML = '';
207+
messages.forEach(m=>{
208+
const div = document.createElement('div');
209+
div.className = 'msg ' + (isMe(m.sender) ? 'me' : '');
210+
div.innerHTML = `<div class="meta">${m.sender}${new Date(m.created_at||m.ts||Date.now()).toLocaleTimeString()}</div><div class="body">${escapeHtml(m.content)}</div>`;
211+
container.appendChild(div);
212+
});
213+
container.scrollTop = container.scrollHeight;
214+
}
215+
function isMe(sender){
216+
if(sessionUser) return sender === sessionUser.id || sender === sessionUser.email;
217+
return sender === deviceId;
218+
}
219+
function escapeHtml(s){ return String(s).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;'); }
220+
221+
/* Polling / fetching */
222+
let pollingTimer = null;
223+
async function fetchMessagesForMe(){
224+
const active = getActiveChatId();
225+
if(!active) return [];
226+
const me = sessionUser ? sessionUser.id : deviceId;
227+
// Endpoint assumed: GET /messages/:userId -> returns messages array (both directions)
228+
const base = document.getElementById('backendUrl').value || BACKEND_API;
229+
try {
230+
const res = await fetch(`${base}/messages/${encodeURIComponent(me)}`);
231+
if(!res.ok) throw new Error('bad res');
232+
const data = await res.json();
233+
// Expect data: [{sender, receiver, content, created_at}, ...]
234+
return data || [];
235+
} catch(err){
236+
console.warn('fetchMessages err', err);
237+
return [];
238+
}
239+
}
240+
241+
async function loadMessagesNow(){
242+
document.getElementById('status').textContent = 'fetching';
243+
const msgs = await fetchMessagesForMe();
244+
// filter by active chat partner
245+
const active = getActiveChatId();
246+
const me = sessionUser ? sessionUser.id : deviceId;
247+
const conversation = msgs.filter(m => (m.sender === active && m.receiver === me) || (m.sender === me && m.receiver === active));
248+
renderMessages(conversation);
249+
document.getElementById('status').textContent = 'idle';
250+
}
251+
252+
function startPolling(){
253+
stopPolling();
254+
pollIntervalMs = Number(document.getElementById('pollInterval').value || 1000);
255+
localStorage.setItem('pollInterval', String(pollIntervalMs));
256+
pollingTimer = setInterval(loadMessagesNow, pollIntervalMs);
257+
}
258+
function stopPolling(){ if(pollingTimer) clearInterval(pollingTimer); pollingTimer = null; }
259+
260+
/* Send message */
261+
document.getElementById('sendBtn').onclick = async () => {
262+
const text = document.getElementById('msgInput').value.trim();
263+
if(!text) return;
264+
const active = getActiveChatId();
265+
if(!active) return alert('Selecciona un contacto primero');
266+
const me = sessionUser ? sessionUser.id : deviceId;
267+
const payload = { sender: me, receiver: active, content: text };
268+
const base = document.getElementById('backendUrl').value || BACKEND_API;
269+
try {
270+
const res = await fetch(`${base}/messages`, {
271+
method: 'POST',
272+
headers: {'Content-Type':'application/json'},
273+
body: JSON.stringify(payload)
274+
});
275+
if(!res.ok) throw new Error('send failed');
276+
document.getElementById('msgInput').value = '';
277+
loadMessagesNow(); // refresca inmediato
278+
} catch(err){
279+
console.error('send err', err);
280+
alert('Error enviando mensaje. Revisa la consola.');
281+
}
282+
};
283+
284+
/* UI events */
285+
document.getElementById('pollInterval').onchange = () => startPolling();
286+
document.getElementById('backendUrl').onchange = ()=>{ document.getElementById('backendUrlLabel').textContent = document.getElementById('backendUrl').value; startPolling(); };
287+
288+
/* Auth (Supabase) - opcional */
289+
async function initSupabase(){
290+
if(!SUPABASE_URL || !SUPABASE_ANON_KEY) return;
291+
// cargar cliente supabase dinamicamente
292+
const script = document.createElement('script');
293+
script.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js';
294+
document.head.appendChild(script);
295+
script.onload = () => {
296+
// globalThis.supabaseJS
297+
supabase = supabaseLib.createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
298+
// check session
299+
const s = supabase.auth.getSession().then(r=>{
300+
const sess = r.data.session;
301+
if(sess && sess.user){
302+
sessionUser = sess.user;
303+
setMyIdDisplay();
304+
}
305+
});
306+
// Optionally subscribe to realtime messages table if permitted (requires policies)
307+
setupRealtimeIfPossible();
308+
};
309+
}
310+
311+
async function setupRealtimeIfPossible(){
312+
if(!supabase) return;
313+
try {
314+
const channel = supabase.channel('public:messages');
315+
channel.on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, payload => {
316+
// payload contains new/old rows depending on event
317+
// we simply reload conversation
318+
loadMessagesNow();
319+
});
320+
await channel.subscribe();
321+
console.log('Supabase realtime subscribed (if allowed).');
322+
} catch(e){ console.warn('Realtime not available or permission denied', e); }
323+
}
324+
325+
document.getElementById('loginGoogle').onclick = async () => {
326+
if(!SUPABASE_URL || !SUPABASE_ANON_KEY) return alert('Pon SUPABASE_URL y SUPABASE_ANON_KEY en el código para usar OAuth.');
327+
if(!supabase) return alert('Inicializa supabase (recarga la página para cargar el cliente).');
328+
await supabase.auth.signInWithOAuth({ provider: 'google' });
329+
};
330+
document.getElementById('loginGithub').onclick = async () => {
331+
if(!SUPABASE_URL || !SUPABASE_ANON_KEY) return alert('Pon SUPABASE_URL y SUPABASE_ANON_KEY en el código para usar OAuth.');
332+
if(!supabase) return alert('Inicializa supabase (recarga la página para cargar el cliente).');
333+
await supabase.auth.signInWithOAuth({ provider: 'github' });
334+
};
335+
document.getElementById('logoutBtn').onclick = async () => {
336+
if(supabase){
337+
await supabase.auth.signOut();
338+
sessionUser = null;
339+
}
340+
// fallback local
341+
deviceId = getOrCreateDeviceId();
342+
setMyIdDisplay();
343+
alert('Desconectado');
344+
};
345+
346+
/* bootstrap */
347+
(function(){
348+
// set initial active chat
349+
const c = loadContacts();
350+
if(c.length) setActiveChatId(c[0]);
351+
else setActiveChatId(null);
352+
353+
// start polling
354+
startPolling();
355+
356+
// init supabase if configured
357+
initSupabase();
358+
359+
// attach keyboard send
360+
document.getElementById('msgInput').addEventListener('keydown', (e)=>{ if(e.key==='Enter') document.getElementById('sendBtn').click(); });
361+
362+
// if there's an active chat, ensure its shown
363+
const active = getActiveChatId();
364+
if(active){
365+
document.getElementById('chatTitle').textContent = `Chat con: ${active}`;
366+
} else {
367+
document.getElementById('messages').innerHTML = '<div class="small">Agrega un contacto o pega tu propio device id en otro dispositivo para chatear.</div>';
368+
}
369+
370+
setMyIdDisplay();
371+
})();
372+
</script>
373+
</body>
374+
</html>

0 commit comments

Comments
 (0)