← back to Linkedin Like Helper

index.html

381 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>LinkedIn Like Helper — manual, TOS-safe</title>
<style>
  :root{
    --bg:#f3f2ef; --card:#fff; --ink:#1d2226; --muted:#56687a;
    --li:#0a66c2; --li-dark:#004182; --line:#d7dce1; --done:#057642;
    --shadow:0 1px 3px rgba(0,0,0,.08),0 1px 2px rgba(0,0,0,.04);
  }
  *{box-sizing:border-box}
  body{margin:0;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
       background:var(--bg);color:var(--ink);padding:24px}
  .wrap{max-width:860px;margin:0 auto}
  header{display:flex;align-items:center;gap:12px;margin-bottom:8px}
  header .badge{width:34px;height:34px;border-radius:7px;background:var(--li);color:#fff;
       display:grid;place-items:center;font-weight:800;font-size:18px}
  h1{font-size:21px;margin:0;font-weight:700}
  .sub{color:var(--muted);margin:2px 0 18px;font-size:13.5px}
  .tabs{display:flex;gap:8px;margin-bottom:16px}
  .tab{padding:8px 16px;border-radius:999px;border:1.5px solid var(--line);background:#fff;cursor:pointer;
       font-size:13.5px;font-weight:600;color:var(--muted);font-family:inherit}
  .tab.active{background:var(--li);color:#fff;border-color:var(--li)}
  .tab:focus-visible{outline:2px solid var(--li);outline-offset:2px}
  .card{background:var(--card);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);
        padding:18px 20px;margin-bottom:16px}
  label{font-weight:600;font-size:13px;display:block;margin-bottom:6px}
  textarea{width:100%;min-height:120px;border:1px solid var(--line);border-radius:8px;padding:11px 12px;
        font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;resize:vertical;color:var(--ink)}
  .row{display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end}
  .grp{flex:1 1 200px}
  select,input[type=number],input[type=text]{width:100%;padding:9px 11px;border:1px solid var(--line);
        border-radius:8px;font-size:14px;background:#fff;color:var(--ink)}
  .btns{display:flex;gap:10px;flex-wrap:wrap;margin-top:4px}
  button{border:0;border-radius:999px;padding:10px 18px;font-size:14px;font-weight:600;cursor:pointer}
  .primary{background:var(--li);color:#fff}
  .primary:hover{background:var(--li-dark)}
  .ghost{background:#fff;color:var(--li);border:1.5px solid var(--li)}
  .ghost:hover{background:#eaf3fb}
  .danger{background:#fff;color:#b11b1b;border:1.5px solid #e3c4c4}
  .danger:hover{background:#fdeeee}
  .stat{display:flex;gap:22px;margin:14px 0 4px;font-size:13.5px;color:var(--muted);flex-wrap:wrap}
  .stat b{color:var(--ink)}
  .bar{height:8px;background:#e6eaee;border-radius:999px;overflow:hidden;margin:10px 0 2px}
  .bar>i{display:block;height:100%;background:var(--done);width:0;transition:width .3s}
  ul.list{list-style:none;margin:8px 0 0;padding:0;max-height:300px;overflow:auto;border-top:1px solid var(--line)}
  ul.list li{display:flex;align-items:center;gap:10px;padding:9px 4px;border-bottom:1px solid #eef1f4;font-size:13.5px}
  ul.list li.done{opacity:.5}
  ul.list li.done .url{text-decoration:line-through}
  .num{width:26px;color:var(--muted);text-align:right;flex:0 0 auto;font-variant-numeric:tabular-nums}
  .url{flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .url a{color:var(--li);text-decoration:none}
  .pill{flex:0 0 auto;font-size:11px;font-weight:700;color:var(--done);border:1px solid #bfe3cd;
        background:#eafaf0;border-radius:999px;padding:2px 8px}
  .mini{flex:0 0 auto;background:#fff;border:1px solid var(--line);border-radius:6px;padding:4px 9px;
        font-size:12px;font-weight:600;cursor:pointer;color:var(--ink)}
  .sources{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px 16px;margin-top:6px}
  .src{display:flex;align-items:center;gap:9px;font-size:13.5px;padding:5px 2px}
  .src input{width:auto}
  .src .tag{color:var(--muted);font-size:11.5px}
  .selrow{display:flex;gap:10px;margin:6px 0 2px}
  .selrow button{padding:5px 12px;font-size:12px}
  .note{font-size:12.5px;color:var(--muted);background:#fffbe9;border:1px solid #f1e3a8;border-radius:8px;
        padding:10px 12px;margin-top:14px}
  .pane{display:none}
  .pane.active{display:block}
  .tos{font-size:12px;color:var(--muted);margin-top:18px;text-align:center}
</style>
</head>
<body>
<div class="wrap">
  <header>
    <div class="badge">in</div>
    <div>
      <h1>LinkedIn Like Helper</h1>
      <div class="sub">Opens streams &amp; posts in batches so <b>you</b> like them by hand. No automation — fully within LinkedIn's terms.</div>
    </div>
  </header>

  <div class="tabs" role="tablist" aria-label="Mode">
    <button type="button" class="tab active" data-mode="feeds" role="tab" aria-selected="true" onclick="setMode('feeds')">📜 Feed pages — scroll &amp; like in place</button>
    <button type="button" class="tab" data-mode="links" role="tab" aria-selected="false" onclick="setMode('links')">🔗 Post links</button>
  </div>

  <!-- ============ FEED-PAGES MODE ============ -->
  <div class="pane active" id="pane-feeds">
    <div class="card">
      <label>Pick the streams to open — each is a fresh river of likable content</label>
      <div class="sources" id="sources"></div>
      <div class="selrow">
        <button class="ghost" onclick="selectAll(true)">Select all</button>
        <button class="ghost" onclick="selectAll(false)">Clear</button>
      </div>

      <label style="margin-top:14px">Add your own — hashtags (<code>#raffia</code>) or full URLs, one per line</label>
      <textarea id="customFeeds" placeholder="#grasscloth&#10;#luxuryinteriors&#10;https://www.linkedin.com/in/some-connection/recent-activity/all/" oninput="persistFeeds();renderFeedStats()"></textarea>
    </div>

    <div class="card">
      <div class="row">
        <div class="grp">
          <label for="fsize">Group size — how many tabs to open per click</label>
          <select id="fsize" onchange="syncCustom('f')">
            <option value="1">1 at a time</option>
            <option value="3" selected>3</option>
            <option value="5">5</option>
            <option value="10">10</option>
            <option value="all">All selected</option>
            <option value="custom">Custom…</option>
          </select>
        </div>
        <div class="grp" id="fcustomWrap" style="display:none">
          <label for="fcustom">Custom count</label>
          <input type="number" id="fcustom" min="1" value="4">
        </div>
        <div class="grp" style="flex:0 0 auto">
          <button class="primary" onclick="openNextFeeds()">▶ Open next group</button>
        </div>
        <div class="grp" style="flex:0 0 auto">
          <button class="danger" onclick="resetFeeds()">Start over</button>
        </div>
      </div>

      <div class="stat">
        <span>Selected: <b id="fTotal">0</b></span>
        <span>Opened: <b id="fDone">0</b></span>
        <span>Left: <b id="fLeft">0</b></span>
      </div>
      <div class="bar"><i id="fBar"></i></div>
      <ul class="list" id="fList"></ul>

      <div class="note">
        <b>First time:</b> Chrome asks to <i>“allow multiple pop-ups”</i> — click Allow once.
        Each tab is a real LinkedIn stream — scroll it and 👍 the posts you like, then close it.
        Opened tabs auto-mark; <b>Start over</b> re-arms the whole set.
      </div>
    </div>
  </div>

  <!-- ============ POST-LINKS MODE ============ -->
  <div class="pane" id="pane-links">
    <div class="card">
      <label for="urls">Post / profile links — one per line  <span style="font-weight:400;color:var(--muted)">(••• → Copy link to post)</span></label>
      <textarea id="urls" placeholder="https://www.linkedin.com/feed/update/urn:li:activity:7300000000000000000/&#10;https://www.linkedin.com/posts/somebody_xyz-activity-7300000000000000000-abcd/"></textarea>
      <div class="btns" style="margin-top:12px">
        <button class="ghost" onclick="loadList()">Load list ↻</button>
        <button class="danger" onclick="resetProgress()">Reset progress</button>
      </div>
    </div>

    <div class="card">
      <div class="row">
        <div class="grp">
          <label for="size">Group size</label>
          <select id="size" onchange="syncCustom('l')">
            <option value="1">1 at a time</option>
            <option value="3">3</option>
            <option value="5" selected>5</option>
            <option value="10">10</option>
            <option value="25">25</option>
            <option value="all">All remaining</option>
            <option value="custom">Custom…</option>
          </select>
        </div>
        <div class="grp" id="lcustomWrap" style="display:none">
          <label for="lcustom">Custom count</label>
          <input type="number" id="lcustom" min="1" value="8">
        </div>
        <div class="grp" style="flex:0 0 auto">
          <button class="primary" onclick="openNext()">▶ Open next group</button>
        </div>
      </div>

      <div class="stat">
        <span>Total: <b id="cTotal">0</b></span>
        <span>Done: <b id="cDone">0</b></span>
        <span>Remaining: <b id="cLeft">0</b></span>
      </div>
      <div class="bar"><i id="barFill"></i></div>
      <ul class="list" id="list"></ul>

      <div class="note">
        <b>First time:</b> allow multiple pop-ups once. Each opened tab is a real post — click 👍 yourself, then close it.
        Opening a post auto-marks it done; use a row's <i>undo</i> if you skipped the like.
      </div>
    </div>
  </div>

  <div class="tos">Manual assist only · your clicks, your account · honors the dw-linkedin “never auto-act” rule</div>
</div>

<script>
/* ---------- shared ---------- */
const TAB_STAGGER_MS = 150;
function openTabs(urls){
  urls.forEach((u,i)=> setTimeout(()=>window.open(u,'_blank','noopener'), i*TAB_STAGGER_MS));
}
function loadKey(k){ try{ return JSON.parse(localStorage.getItem(k))||{} }catch(e){ return {} } }
function syncCustom(which){
  const sel = document.getElementById(which==='f'?'fsize':'size').value;
  document.getElementById(which==='f'?'fcustomWrap':'lcustomWrap').style.display = sel==='custom' ? '' : 'none';
}
function groupSizeFrom(selId, customId){
  const v = document.getElementById(selId).value;
  if(v==='all') return Infinity;
  if(v==='custom') return Math.max(1, parseInt(document.getElementById(customId).value)||1);
  return parseInt(v);
}

/* ---------- mode switch ---------- */
function setMode(m){
  document.querySelectorAll('.tab').forEach(t=>{
    const on = t.dataset.mode===m;
    t.classList.toggle('active', on);
    t.setAttribute('aria-selected', on ? 'true' : 'false');
  });
  document.getElementById('pane-feeds').classList.toggle('active', m==='feeds');
  document.getElementById('pane-links').classList.toggle('active', m==='links');
  localStorage.setItem('lk_mode', m);
}

/* ======================================================== */
/*  FEED-PAGES MODE                                         */
/* ======================================================== */
const HASH = h => 'https://www.linkedin.com/feed/hashtag/?keywords='+encodeURIComponent(h);
const PRESETS = [
  {label:'🏠 My Home Feed',      url:'https://www.linkedin.com/feed/',            tag:'feed',  on:true},
  {label:'🔔 Notifications',     url:'https://www.linkedin.com/notifications/',   tag:'feed',  on:false},
  {label:'👥 My Network',        url:'https://www.linkedin.com/mynetwork/',       tag:'feed',  on:false},
  {label:'#interiordesign',      url:HASH('interiordesign'),     tag:'hashtag', on:true},
  {label:'#interiordesigner',    url:HASH('interiordesigner'),   tag:'hashtag', on:true},
  {label:'#wallcoverings',       url:HASH('wallcoverings'),      tag:'hashtag', on:true},
  {label:'#wallpaper',           url:HASH('wallpaper'),          tag:'hashtag', on:true},
  {label:'#commercialdesign',    url:HASH('commercialdesign'),   tag:'hashtag', on:false},
  {label:'#hospitalitydesign',   url:HASH('hospitalitydesign'),  tag:'hashtag', on:false},
  {label:'#luxuryinteriors',     url:HASH('luxuryinteriors'),    tag:'hashtag', on:false},
  {label:'#designtrends',        url:HASH('designtrends'),       tag:'hashtag', on:false},
  {label:'#architecture',        url:HASH('architecture'),       tag:'hashtag', on:false},
];
const FOPEN_KEY = 'lk_feed_opened';
function loadFeedOpened(){ const v = loadKey(FOPEN_KEY); return new Set(Array.isArray(v.urls) ? v.urls : []); }
function saveFeedOpened(){ localStorage.setItem(FOPEN_KEY, JSON.stringify({ urls:[...fOpened] })); }
let fOpened = loadFeedOpened();   // urls opened, persisted across reloads

function loadFeedPrefs(){ return loadKey('lk_feed_prefs'); }
function persistFeeds(){
  const checked = {};
  document.querySelectorAll('#sources input[type=checkbox]').forEach(c=>checked[c.value]=c.checked);
  localStorage.setItem('lk_feed_prefs', JSON.stringify({
    checked, custom: document.getElementById('customFeeds').value
  }));
}
function customFeedUrls(){
  return document.getElementById('customFeeds').value.split(/\n+/).map(s=>s.trim()).filter(Boolean)
    .map(s => s.startsWith('#') ? HASH(s.slice(1)) : (s.startsWith('http') ? s : HASH(s)));
}
function selectedFeeds(){
  const urls = [];
  document.querySelectorAll('#sources input[type=checkbox]').forEach(c=>{ if(c.checked) urls.push(c.value); });
  customFeedUrls().forEach(u=>{ if(!urls.includes(u)) urls.push(u); });
  return urls;
}
function renderSources(){
  const prefs = loadFeedPrefs();
  const host = document.getElementById('sources'); host.innerHTML='';
  PRESETS.forEach(p=>{
    const isOn = prefs.checked && (p.url in prefs.checked) ? prefs.checked[p.url] : p.on;
    const div = document.createElement('label'); div.className='src';
    div.innerHTML = '<input type="checkbox" value="'+p.url+'" '+(isOn?'checked':'')+'>'+
                    '<span>'+p.label+'</span>';
    div.querySelector('input').onchange = ()=>{ persistFeeds(); renderFeedStats(); };
    host.appendChild(div);
  });
  if(prefs.custom) document.getElementById('customFeeds').value = prefs.custom;
}
function selectAll(on){
  document.querySelectorAll('#sources input[type=checkbox]').forEach(c=>c.checked=on);
  persistFeeds(); renderFeedStats();
}
function feedLabel(url){
  const p = PRESETS.find(pr=>pr.url===url);
  if(p) return p.label;
  const m = url.match(/[?&]keywords=([^&]+)/);
  if(m) return '#'+decodeURIComponent(m[1]);
  return url;
}
function reopenFeed(url){ openTabs([url]); fOpened.add(url); saveFeedOpened(); renderFeedStats(); }
function renderFeedStats(){
  const sel = selectedFeeds();
  const opened = sel.filter(u=>fOpened.has(u)).length;
  document.getElementById('fTotal').textContent = sel.length;
  document.getElementById('fDone').textContent  = opened;
  document.getElementById('fLeft').textContent  = sel.length-opened;
  document.getElementById('fBar').style.width = sel.length? (opened/sel.length*100)+'%' : '0';
  const ul=document.getElementById('fList'); ul.innerHTML='';
  sel.forEach((u,i)=>{
    const done=fOpened.has(u);
    const li=document.createElement('li'); if(done) li.className='done';
    li.innerHTML='<span class="num">'+(i+1)+'</span>'+
      '<span class="url"><a href="'+u+'" target="_blank" rel="noopener">'+feedLabel(u)+'</a></span>'+
      (done?'<span class="pill">opened</span>':'')+
      '<button class="mini" data-u="'+encodeURIComponent(u)+'">re-open</button>';
    ul.appendChild(li);
  });
  ul.querySelectorAll('.mini').forEach(b=>b.onclick=()=>reopenFeed(decodeURIComponent(b.dataset.u)));
}
function openNextFeeds(){
  const sel = selectedFeeds();
  if(!sel.length){ alert('Pick at least one stream first.'); return; }
  const pending = sel.filter(u=>!fOpened.has(u));
  if(!pending.length){ alert('Opened them all 🎉  Hit “Start over” to re-arm.'); return; }
  const n = Math.min(groupSizeFrom('fsize','fcustom'), pending.length);
  const batch = pending.slice(0,n);
  openTabs(batch);
  batch.forEach(u=>fOpened.add(u));
  saveFeedOpened();
  renderFeedStats();
}
function resetFeeds(){ fOpened.clear(); saveFeedOpened(); renderFeedStats(); }

/* ======================================================== */
/*  POST-LINKS MODE                                         */
/* ======================================================== */
const KEY='lk_like_helper_v1';
let items=[];
function loadLS(){ return loadKey(KEY); }
function saveLS(s){ localStorage.setItem(KEY, JSON.stringify(s)); }
function loadList(){
  const lines = document.getElementById('urls').value.split(/\n+/).map(s=>s.trim()).filter(Boolean);
  const seen=new Set(), urls=[];
  for(const u of lines){ if(!seen.has(u)){ seen.add(u); urls.push(u); } }
  const doneMap=(loadLS().done)||{};
  items = urls.map(u=>({url:u, done:!!doneMap[u]}));
  persistLinks(); renderLinks();
}
function persistLinks(){
  const doneMap={}; items.forEach(i=>{ if(i.done) doneMap[i.url]=1; });
  saveLS({ raw: document.getElementById('urls').value, done: doneMap });
}
function openNext(){
  if(!items.length){ loadList(); }
  const pending=items.filter(i=>!i.done);
  if(!pending.length){ alert('All done 🎉  Use “Reset progress” to go again.'); return; }
  const n=Math.min(groupSizeFrom('size','lcustom'), pending.length);
  const batch=pending.slice(0,n);
  openTabs(batch.map(i=>i.url));
  batch.forEach(i=>i.done=true);
  persistLinks(); renderLinks();
}
function toggleDone(url){ const it=items.find(i=>i.url===url); if(it){ it.done=!it.done; persistLinks(); renderLinks(); } }
function resetProgress(){ items.forEach(i=>i.done=false); persistLinks(); renderLinks(); }
function renderLinks(){
  const total=items.length, done=items.filter(i=>i.done).length;
  document.getElementById('cTotal').textContent=total;
  document.getElementById('cDone').textContent=done;
  document.getElementById('cLeft').textContent=total-done;
  document.getElementById('barFill').style.width = total? (done/total*100)+'%':'0';
  const ul=document.getElementById('list'); ul.innerHTML='';
  items.forEach((it,i)=>{
    const li=document.createElement('li'); if(it.done) li.className='done';
    li.innerHTML='<span class="num">'+(i+1)+'</span>'+
      '<span class="url"><a href="'+it.url+'" target="_blank" rel="noopener">'+it.url+'</a></span>'+
      (it.done?'<span class="pill">done</span>':'')+
      '<button class="mini" data-u="'+encodeURIComponent(it.url)+'">'+(it.done?'undo':'done')+'</button>';
    ul.appendChild(li);
  });
  ul.querySelectorAll('.mini').forEach(b=>b.onclick=()=>toggleDone(decodeURIComponent(b.dataset.u)));
}

/* ---------- init ---------- */
(function init(){
  renderSources(); renderFeedStats(); syncCustom('f'); syncCustom('l');
  const prev=loadLS(); if(prev.raw){ document.getElementById('urls').value=prev.raw; loadList(); }
  setMode(localStorage.getItem('lk_mode')||'feeds');
})();
</script>
</body>
</html>