<!doctype html><html lang="es"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Picapon — Mundo Gaturro (demo)</title> <style> /* Estética noventera/2000s: colores pastel, bordes redondeados y fuente con sombra */ :root{ --bg:#f5f4fb; --card:#ffffff; --accent:#ff7fbf; --muted:#777; --glass: rgba(255,255,255,0.6); } *{box-sizing:border-box} body{font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background:linear-gradient(180deg,#f8f0ff 0%, #f5f8ff 100%); color:#222; margin:0; padding:20px} .wrap{max-width:1100px;margin:0 auto;display:grid;grid-template-columns:280px 1fr 260px;gap:18px}
header{grid-column:1/-1; display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-radius:12px;background:linear-gradient(90deg,#fff,#fff7fb);box-shadow:0 6px 20px rgba(0,0,0,0.06)} header h1{margin:0;font-size:20px;letter-spacing:1px} header .brand{display:flex;gap:12px;align-items:center} .logo{width:44px;height:44px;background:var(--accent);display:flex;align-items:center;justify-content:center;border-radius:10px;color:white;font-weight:700;box-shadow:0 4px 10px rgba(0,0,0,0.08)}
.card{background:var(--card);padding:12px;border-radius:12px;box-shadow:0 6px 18px rgba(0,0,0,0.04)} .sidebar .profile{display:flex;gap:12px;align-items:center} .avatar{width:64px;height:64px;border-radius:12px;background:linear-gradient(180deg,#fff,#eee);display:flex;align-items:center;justify-content:center;font-weight:700;color:#333} .btn{display:inline-block;padding:8px 10px;border-radius:10px;background:var(--accent);color:white;text-decoration:none;cursor:pointer;border:none} .muted{color:var(--muted);font-size:13px}
/* composer */ .composer textarea{width:100%;min-height:80px;border-radius:8px;border:1px solid #eee;padding:10px;font-size:14px;resize:vertical} .composer .actions{display:flex;justify-content:space-between;align-items:center;margin-top:8px}
/* feed */ .feed .post{margin-bottom:12px;padding:12px;border-radius:10px} .post .meta{display:flex;gap:12px;align-items:center} .post .content{margin-top:8px;white-space:pre-wrap} .post img.media{max-width:100%;border-radius:8px;margin-top:8px} .post .post-actions{display:flex;gap:8px;margin-top:10px}
/* right column */ .search input{width:100%;padding:8px;border-radius:8px;border:1px solid #eee} .users-list{display:flex;flex-direction:column;gap:8px;margin-top:8px} .user-item{display:flex;gap:8px;align-items:center}
footer{grid-column:1/-1;text-align:center;padding:12px;color:var(--muted);font-size:13px}
/* responsive */ @media (max-width:900px){.wrap{grid-template-columns:1fr;padding:0}.right,.left{order:2}.feed{order:1}} </style></head><body> <div class="wrap"> <header class="card"> <div class="brand"> <div class="logo">P</div> <div> <h1>Picapon</h1> <div class="muted">red social demo • estilo Mundo Gaturro</div> </div> </div> <div id="top-controls"> <span id="welcomeText" class="muted">No conectado</span> <button id="logoutBtn" class="btn" style="display:none">Cerrar sesión</button> </div> </header>
<!-- Left: perfil rápido --> <aside class="left card sidebar"> <div class="profile"> <div id="sideAvatar" class="avatar">?</div> <div> <div id="sideName" style="font-weight:700">Invitado</div> <div id="sideHandle" class="muted">@visitante</div> </div> </div>
<hr style="margin:12px 0;border:none;border-top:1px solid #f0eef3">
<div id="authArea"> <div style="margin-bottom:8px"><strong>Iniciar / Registrar</strong></div> <input id="inputName" placeholder="Nombre" style="width:100%;padding:8px;border-radius:8px;border:1px solid #eee;margin-bottom:6px"> <input id="inputHandle" placeholder="Usuario (sin @)" style="width:100%;padding:8px;border-radius:8px;border:1px solid #eee;margin-bottom:6px"> <label class="muted" style="font-size:12px">Avatar (opcional)</label> <input id="inputAvatarFile" type="file" accept="image/*" style="width:100%;margin-top:6px"> <div style="display:flex;gap:8px;margin-top:8px"> <button id="loginBtn" class="btn">Entrar / Crear</button> <button id="demoBtn" class="btn" style="background:#6fc3ff">Demo</button> </div> </div>
<hr style="margin:12px 0;border:none;border-top:1px solid #f0eef3">
<div> <strong>Estadísticas</strong> <div style="margin-top:8px" class="muted">Publicaciones: <span id="statPosts">0</span></div> <div class="muted">Usuarios: <span id="statUsers">0</span></div> </div> </aside>
<!-- Middle: feed --> <main class="feed"> <section class="card composer"> <div style="display:flex;gap:12px;align-items:flex-start"> <div id="composeAvatar" class="avatar">?</div> <div style="flex:1"> <textarea id="postText" placeholder="¿Qué está pasando en Picapon?" maxlength="500"></textarea> <div class="actions"> <div> <input id="postImage" type="file" accept="image/*"> </div> <div> <button id="postBtn" class="btn">Publicar</button> </div> </div> </div> </div> </section>
<section id="feedList" class="card" style="margin-top:12px"> <h3 style="margin-top:0">Inicio</h3> <div id="postsContainer"></div> </section> </main>
<!-- Right: buscador / usuarios --> <aside class="right card"> <div class="search"> <input id="searchInput" placeholder="Buscar usuarios o texto..."> </div> <div style="margin-top:12px"> <strong>Usuarios</strong> <div id="usersList" class="users-list"></div> </div> </aside>
<footer class="muted">Picapon — demo local • Los datos se guardan en tu navegador (localStorage)</footer> </div>
<script> // --- Simple data layer usando localStorage --- const STORE_USERS = 'picapon_users_v1'; const STORE_POSTS = 'picapon_posts_v1'; const STORE_SESSION = 'picapon_session_v1';
function loadJSON(key, fallback){ try{ return JSON.parse(localStorage.getItem(key))||fallback }catch(e){return fallback} } function saveJSON(key, value){ localStorage.setItem(key, JSON.stringify(value)); }
let users = loadJSON(STORE_USERS, {}); let posts = loadJSON(STORE_POSTS, []); let session = loadJSON(STORE_SESSION, null);
// --- Demo seed if empty --- if(Object.keys(users).length===0){ const seed = { 'gatito': {name:'Gatilín', handle:'gatito', avatar:null}, 'nico':{name:'Nico', handle:'nico', avatar:null} }; users = seed; saveJSON(STORE_USERS, users); posts = [ {id:genId(), author:'gatito', text:'Bienvenido a Picapon — recuerda que es una demo.', ts:Date.now()-3600e3, likes:1, comments:[], media:null}, {id:genId(), author:'nico', text:'¿Quién quiere jugar con Gati? 🐱', ts:Date.now()-1800e3, likes:2, comments:[], media:null} ]; saveJSON(STORE_POSTS, posts); }
// --- Helpers --- function genId(){ return Math.random().toString(36).slice(2,9); } function timeAgo(ts){ const s = Math.floor((Date.now()-ts)/1000); if(s<60) return s+'s'; if(s<3600) return Math.floor(s/60)+'m'; if(s<86400) return Math.floor(s/3600)+'h'; return Math.floor(s/86400)+'d'; }
// --- UI refs --- const sideAvatar = document.getElementById('sideAvatar'); const sideName = document.getElementById('sideName'); const sideHandle = document.getElementById('sideHandle'); const welcomeText = document.getElementById('welcomeText'); const logoutBtn = document.getElementById('logoutBtn'); const postBtn = document.getElementById('postBtn'); const postsContainer = document.getElementById('postsContainer'); const statPosts = document.getElementById('statPosts'); const statUsers = document.getElementById('statUsers'); const usersList = document.getElementById('usersList'); const postText = document.getElementById('postText'); const postImage = document.getElementById('postImage'); const composeAvatar = document.getElementById('composeAvatar'); const inputName = document.getElementById('inputName'); const inputHandle = document.getElementById('inputHandle'); const inputAvatarFile = document.getElementById('inputAvatarFile'); const loginBtn = document.getElementById('loginBtn'); const demoBtn = document.getElementById('demoBtn'); const searchInput = document.getElementById('searchInput');
// --- Render functions --- function renderStats(){ statPosts.innerText = posts.length; statUsers.innerText = Object.keys(users).length; }
function renderUserList(filter=''){ usersList.innerHTML=''; const entries = Object.values(users).filter(u=> (u.name+u.handle).toLowerCase().includes(filter.toLowerCase())); entries.sort((a,b)=> a.handle.localeCompare(b.handle)); entries.forEach(u=>{ const el = document.createElement('div'); el.className='user-item'; const av = document.createElement('div'); av.className='avatar'; av.style.width='40px'; av.style.height='40px'; av.innerText = u.avatar? '' : (u.name||u.handle).slice(0,1).toUpperCase(); if(u.avatar){ const i = document.createElement('img'); i.src=u.avatar; i.style.width='100%'; i.style.height='100%'; i.style.objectFit='cover'; i.style.borderRadius='8px'; av.innerHTML=''; av.appendChild(i); } const info = document.createElement('div'); info.innerHTML = `<div style="font-weight:700">${escapeHtml(u.name||'Sin nombre')}</div><div class="muted">@${escapeHtml(u.handle)}</div>`; const btn = document.createElement('button'); btn.className='btn'; btn.style.marginLeft='auto'; btn.innerText='Seguir'; btn.onclick = ()=>{ alert('Funcionalidad de seguir es demostrativa.'); } el.appendChild(av); el.appendChild(info); el.appendChild(btn); usersList.appendChild(el); }); }
function renderHeader(){ if(session){ welcomeText.innerText = `Hola, ${session.name}`; logoutBtn.style.display='inline-block'; } else { welcomeText.innerText='No conectado'; logoutBtn.style.display='none'; } }
function renderProfileSidebar(){ if(session){ sideName.innerText = session.name; sideHandle.innerText = '@'+session.handle; sideAvatar.innerText=''; composeAvatar.innerText=''; if(session.avatar){ const i=document.createElement('img'); i.src=session.avatar; i.style.width='100%'; i.style.height='100%'; i.style.objectFit='cover'; i.style.borderRadius='8px'; sideAvatar.innerHTML=''; sideAvatar.appendChild(i); const j=i.cloneNode(); j.style.width='64px'; j.style.height='64px'; composeAvatar.innerHTML=''; composeAvatar.appendChild(j); } } else { sideName.innerText='Invitado'; sideHandle.innerText='@visitante'; sideAvatar.innerText='?'; composeAvatar.innerText='?'; } }
function renderPosts(filterText=''){ postsContainer.innerHTML=''; let list = posts.slice().sort((a,b)=> b.ts-a.ts); if(filterText){ list = list.filter(p => p.text.toLowerCase().includes(filterText.toLowerCase()) || (users[p.author] && (users[p.author].name+users[p.author].handle).toLowerCase().includes(filterText.toLowerCase())) ); } list.forEach(p=>{ const el = document.createElement('div'); el.className='post'; el.classList.add('card'); const author = users[p.author] || {name:'Desconocido', handle:p.author, avatar:null}; const meta = document.createElement('div'); meta.className='meta'; const av = document.createElement('div'); av.className='avatar'; av.style.width='48px'; av.style.height='48px'; av.innerText = author.avatar? '' : (author.name||author.handle||'?').slice(0,1).toUpperCase(); if(author.avatar){ const i=document.createElement('img'); i.src=author.avatar; i.style.width='100%'; i.style.height='100%'; i.style.objectFit='cover'; i.style.borderRadius='8px'; av.innerHTML=''; av.appendChild(i); } const m2 = document.createElement('div'); m2.innerHTML=`<div style="font-weight:700">${escapeHtml(author.name||'Sin nombre')} <span class="muted">@${escapeHtml(author.handle)}</span></div><div class="muted">${timeAgo(p.ts)}</div>`; meta.appendChild(av); meta.appendChild(m2); el.appendChild(meta); const content = document.createElement('div'); content.className='content'; content.innerText = p.text || ''; el.appendChild(content); if(p.media){ const img = document.createElement('img'); img.className='media'; img.src=p.media; el.appendChild(img); } const actions = document.createElement('div'); actions.className='post-actions'; const likeBtn = document.createElement('button'); likeBtn.className='btn'; likeBtn.style.background='#ffd24d'; likeBtn.innerText = `❤ ${p.likes||0}`; likeBtn.onclick = ()=>{ p.likes = (p.likes||0)+1; saveJSON(STORE_POSTS, posts); renderPosts(searchInput.value); renderStats(); } const commentBtn = document.createElement('button'); commentBtn.className='btn'; commentBtn.style.background='#9be7a3'; commentBtn.innerText = `💬 ${p.comments.length}`; commentBtn.onclick = ()=>{ const txt = prompt('Agregar comentario:'); if(txt){ p.comments.push({id:genId(), author: session? session.handle : 'anon', text:txt, ts:Date.now()}); saveJSON(STORE_POSTS, posts); renderPosts(searchInput.value); } } actions.appendChild(likeBtn); actions.appendChild(commentBtn); if(session && session.handle===p.author){ const del = document.createElement('button'); del.className='btn'; del.style.background='#ff8b8b'; del.innerText='Eliminar'; del.onclick = ()=>{ if(confirm('Eliminar publicación?')){ posts = posts.filter(x=> x.id!==p.id); saveJSON(STORE_POSTS, posts); renderPosts(searchInput.value); renderStats(); } } actions.appendChild(del); } el.appendChild(actions);
// comments preview if(p.comments && p.comments.length){ const cprev = document.createElement('div'); cprev.style.marginTop='8px'; cprev.style.paddingTop='8px'; cprev.style.borderTop='1px dashed #eee'; p.comments.slice(-3).forEach(c=>{ const cu = users[c.author] || {name:c.author, handle:c.author}; const line = document.createElement('div'); line.innerHTML = `<strong>${escapeHtml(cu.name||cu.handle)}</strong> <span class="muted">@${escapeHtml(cu.handle||c.author)}</span>: ${escapeHtml(c.text)}`; cprev.appendChild(line); }); el.appendChild(cprev); }
postsContainer.appendChild(el); }); }
// --- Auth --- function loginFromInputs(){ const name = inputName.value.trim(); const handle = (inputHandle.value.trim()||'').replace(/@/g,''); if(!name||!handle){ alert('Ingresa nombre y usuario.'); return; } if(users[handle] && users[handle].name && users[handle].name!==name){ if(!confirm('El usuario existe con otro nombre. ¿Usar igual?')) return; } const user = users[handle] || {name, handle}; user.name = name; user.handle = handle; // avatar const file = inputAvatarFile.files[0]; if(file){ const reader = new FileReader(); reader.onload = e=>{ user.avatar = e.target.result; users[handle]=user; saveJSON(STORE_USERS, users); setSession(user); renderAll(); }; reader.readAsDataURL(file); } else{ users[handle]=user; saveJSON(STORE_USERS, users); setSession(user); renderAll(); } }
function setSession(user){ session = user; saveJSON(STORE_SESSION, session); renderHeader(); renderProfileSidebar(); } function logout(){ session=null; localStorage.removeItem(STORE_SESSION); renderAll(); }
// --- Posting --- postBtn.onclick = ()=>{ if(!session){ alert('Debes iniciar sesión para publicar.'); return; } const text = postText.value.trim(); if(!text && !postImage.files[0]){ alert('Escribe algo o sube una imagen.'); return; } const newPost = { id:genId(), author:session.handle, text, ts:Date.now(), likes:0, comments:[], media:null }; const file = postImage.files[0]; if(file){ const r=new FileReader(); r.onload = e=>{ newPost.media = e.target.result; posts.push(newPost); saveJSON(STORE_POSTS, posts); postText.value=''; postImage.value=''; renderAll(); }; r.readAsDataURL(file); } else{ posts.push(newPost); saveJSON(STORE_POSTS, posts); postText.value=''; renderAll(); } }
// --- small helpers --- function escapeHtml(s){ if(!s) return ''; return s.replace(/[&<>"']/g, c=>({ '&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
// --- events --- loginBtn.onclick = loginFromInputs; demoBtn.onclick = ()=>{ setSession({name:'Demo', handle:'demo', avatar:null}); renderAll(); }; logoutBtn.onclick = ()=>{ logout(); }; searchInput.oninput = ()=>{ renderPosts(searchInput.value); renderUserList(searchInput.value); }
// quick load current session if(session){ /* already loaded */ }
function renderAll(){ renderHeader(); renderProfileSidebar(); renderPosts(); renderUserList(); renderStats(); }
renderAll(); </script></body></html>