Initial commit

This commit is contained in:
2026-04-12 08:54:19 +00:00
commit 18016013d7
9 changed files with 1262 additions and 0 deletions

288
public/index.html Normal file
View File

@@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LinkedIn Post Formatter</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#f3f2ef;--surface:#fff;--border:#e0ddd6;--border2:#c8c5be;
--text:#1a1a18;--muted:#666;--radius:8px;--radius-lg:12px;
}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column;}
header{background:#fff;border-bottom:1px solid var(--border);padding:13px 24px;display:flex;align-items:center;gap:12px;}
.li-logo{width:28px;height:28px;background:#0077b5;border-radius:5px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:15px;flex-shrink:0;}
header h1{font-size:16px;font-weight:600;}
header span{font-size:13px;color:var(--muted);margin-left:auto;}
.layout{display:flex;flex:1;overflow:hidden;max-height:calc(100vh - 54px);}
.left{width:360px;flex-shrink:0;display:flex;flex-direction:column;border-right:1px solid var(--border);background:#fff;}
.panel-hd{padding:11px 16px;border-bottom:1px solid var(--border);font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;}
.toolbar{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;flex-wrap:wrap;gap:4px;background:#fafaf8;}
.toolbar button{font-size:13px;padding:4px 10px;border:1px solid var(--border2);background:#fff;color:var(--text);border-radius:var(--radius);cursor:pointer;font-family:inherit;}
.toolbar button:hover{background:var(--border);}
.sep{width:1px;background:var(--border2);margin:2px 2px;align-self:stretch;}
textarea{flex:1;width:100%;padding:16px;font-size:15px;line-height:1.75;border:none;outline:none;resize:none;background:#fff;color:var(--text);font-family:inherit;}
.left-foot{padding:9px 16px;border-top:1px solid var(--border);display:flex;align-items:center;gap:8px;}
.counter{font-size:13px;color:var(--muted);}
.counter strong{color:var(--text);}
.counter.warn strong{color:#b45309;}
.counter.danger strong{color:#c0392b;}
.prog{flex:1;height:4px;background:var(--border);border-radius:2px;overflow:hidden;}
.prog-fill{height:100%;background:#2d7a3a;border-radius:2px;transition:width .15s,background .15s;}
button.gen{background:#0a66c2;color:#fff;border:none;border-radius:var(--radius);font-size:13px;font-weight:500;padding:6px 16px;cursor:pointer;font-family:inherit;white-space:nowrap;}
button.gen:hover{background:#094fa3;}
.right{flex:1;overflow-y:auto;padding:20px;}
.hint{font-size:13px;color:var(--muted);margin-bottom:16px;}
.hint kbd{background:#eee;padding:1px 5px;border-radius:4px;font-size:12px;border:1px solid var(--border2);}
.cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:14px;}
.variant-card{background:#fff;border-radius:var(--radius-lg);border:1px solid var(--border);overflow:hidden;}
.variant-label{padding:7px 13px;font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--border);background:#fafaf8;}
.li-post{padding:12px 13px 10px;}
.li-post-header{display:flex;align-items:center;gap:9px;margin-bottom:9px;}
.li-avatar{width:36px;height:36px;border-radius:50%;background:#0077b5;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:13px;flex-shrink:0;}
.li-name{font-size:13px;font-weight:600;line-height:1.3;}
.li-sub{font-size:11px;color:var(--muted);}
.li-text{font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word;color:#1a1a18;max-height:200px;overflow:hidden;position:relative;}
.li-text.expanded{max-height:none;}
.li-fade{position:absolute;bottom:0;left:0;right:0;height:36px;background:linear-gradient(transparent,#fff);}
.li-more{font-size:12px;color:#0a66c2;cursor:pointer;margin-top:3px;}
.card-foot{display:flex;align-items:center;justify-content:space-between;padding:8px 13px;border-top:1px solid var(--border);}
.li-actions{display:flex;gap:0;}
.li-act{font-size:11px;color:var(--muted);padding:3px 7px;border-radius:4px;cursor:pointer;}
.li-act:hover{background:var(--bg);}
.copy-btn{font-size:12px;padding:5px 11px;border:1px solid var(--border2);background:#fff;color:var(--text);border-radius:var(--radius);cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:4px;}
.copy-btn:hover{background:var(--bg);}
.copy-btn.copied{background:#d1fae5;color:#065f46;border-color:#6ee7b7;}
.empty-state{text-align:center;padding:80px 20px;color:var(--muted);}
.empty-state p{font-size:15px;margin-bottom:8px;color:var(--text);}
.empty-state small{font-size:13px;}
@media(max-width:768px){
.layout{flex-direction:column;max-height:none;overflow:visible;}
.left{width:100%;}
.right{padding:14px;}
}
</style>
</head>
<body>
<header>
<div class="li-logo">in</div>
<h1>LinkedIn Post Formatter</h1>
<span>self-hosted · kostenlos · kein Login</span>
</header>
<div class="layout">
<div class="left">
<div class="panel-hd">Eingabe</div>
<div class="toolbar">
<button onclick="wrapSel('**','**')"><b>B</b> Fett</button>
<button onclick="wrapSel('_','_')"><i>I</i> Kursiv</button>
<div class="sep"></div>
<button onclick="ins('• ')"></button>
<button onclick="ins('→ ')"></button>
<div class="sep"></div>
<button onclick="ins('👉 ')">👉</button>
<button onclick="ins('✅ ')"></button>
<button onclick="ins('❌ ')"></button>
<button onclick="ins('⚠️ ')">⚠️</button>
<button onclick="ins('💡')">💡</button>
<button onclick="ins('🔥')">🔥</button>
<button onclick="ins('🚀')">🚀</button>
<button onclick="ins('⚡')"></button>
<button onclick="ins('📌')">📌</button>
<button onclick="ins('🎯')">🎯</button>
<button onclick="ins('📊')">📊</button>
<button onclick="ins('🔧')">🔧</button>
<button onclick="ins('💰')">💰</button>
<button onclick="ins('📈')">📈</button>
<button onclick="ins('🤝')">🤝</button>
<button onclick="ins('👀')">👀</button>
<button onclick="ins('💬')">💬</button>
<button onclick="ins('✍️')">✍️</button>
<button onclick="ins('🏆')">🏆</button>
<button onclick="ins('❓')"></button>
<button onclick="ins('1⃣ ')">1</button>
<button onclick="ins('2⃣ ')">2</button>
<button onclick="ins('3⃣ ')">3</button>
<button onclick="ins('🧠')">🧠</button>
<button onclick="ins('📣')">📣</button>
<button onclick="ins('🔑')">🔑</button>
<button onclick="ins('👆 ')">👆</button>
<button onclick="ins('☑️ ')">☑️</button>
<button onclick="ins('🌍')">🌍</button>
<button onclick="ins('⏰')"></button>
<button onclick="ins('💎')">💎</button>
<div class="sep"></div>
<button onclick="clearAll()"></button>
</div>
<textarea id="ta" placeholder="Text eingeben...
**Doppelstern** = Fett
_Unterstrich_ = Kursiv
Leerzeile = Absatz in LinkedIn"></textarea>
<div class="left-foot">
<div class="counter" id="ctr"><strong>0</strong>&nbsp;/ 3000</div>
<div class="prog"><div class="prog-fill" id="bar" style="width:0%"></div></div>
<button class="gen" onclick="generate()">Vorschau ↗</button>
</div>
</div>
<div class="right" id="right">
<div class="empty-state">
<p>Text eingeben → „Vorschau" klicken</p>
<small>6 Schriftvarianten als LinkedIn-Karten — direkt per „Copy text" in die Zwischenablage</small>
</div>
</div>
</div>
<script>
const B={A:'𝗔',B:'𝗕',C:'𝗖',D:'𝗗',E:'𝗘',F:'𝗙',G:'𝗚',H:'𝗛',I:'𝗜',J:'𝗝',K:'𝗞',L:'𝗟',M:'𝗠',N:'𝗡',O:'𝗢',P:'𝗣',Q:'𝗤',R:'𝗥',S:'𝗦',T:'𝗧',U:'𝗨',V:'𝗩',W:'𝗪',X:'𝗫',Y:'𝗬',Z:'𝗭',a:'𝗮',b:'𝗯',c:'𝗰',d:'𝗱',e:'𝗲',f:'𝗳',g:'𝗴',h:'𝗵',i:'𝗶',j:'𝗷',k:'𝗸',l:'𝗹',m:'𝗺',n:'𝗻',o:'𝗼',p:'𝗽',q:'𝗾',r:'𝗿',s:'𝘀',t:'𝘁',u:'𝘂',v:'𝘃',w:'𝘄',x:'𝘅',y:'𝘆',z:'𝘇','0':'𝟬','1':'𝟭','2':'𝟮','3':'𝟯','4':'𝟰','5':'𝟱','6':'𝟲','7':'𝟳','8':'𝟴','9':'𝟵'};
const I={A:'𝘈',B:'𝘉',C:'𝘊',D:'𝘋',E:'𝘌',F:'𝘍',G:'𝘎',H:'𝘏',I:'𝘐',J:'𝘑',K:'𝘒',L:'𝘓',M:'𝘔',N:'𝘕',O:'𝘖',P:'𝘗',Q:'𝘘',R:'𝘙',S:'𝘚',T:'𝘛',U:'𝘜',V:'𝘝',W:'𝘞',X:'𝘟',Y:'𝘠',Z:'𝘡',a:'𝘢',b:'𝘣',c:'𝘤',d:'𝘥',e:'𝘦',f:'𝘧',g:'𝘨',h:'𝘩',i:'𝘪',j:'𝘫',k:'𝘬',l:'𝘭',m:'𝘮',n:'𝘯',o:'𝘰',p:'𝘱',q:'𝘲',r:'𝘳',s:'𝘴',t:'𝘵',u:'𝘶',v:'𝘷',w:'𝘸',x:'𝘹',y:'𝘺',z:'𝘻'};
const BI={A:'𝘼',B:'𝘽',C:'𝘾',D:'𝘿',E:'𝙀',F:'𝙁',G:'𝙂',H:'𝙃',I:'𝙄',J:'𝙅',K:'𝙆',L:'𝙇',M:'𝙈',N:'𝙉',O:'𝙊',P:'𝙋',Q:'𝙌',R:'𝙍',S:'𝙎',T:'𝙏',U:'𝙐',V:'𝙑',W:'𝙒',X:'𝙓',Y:'𝙔',Z:'𝙕',a:'𝙖',b:'𝙗',c:'𝙘',d:'𝙙',e:'𝙚',f:'𝙛',g:'𝙜',h:'𝙝',i:'𝙞',j:'𝙟',k:'𝙠',l:'𝙡',m:'𝙢',n:'𝙣',o:'𝙤',p:'𝙥',q:'𝙦',r:'𝙧',s:'𝙨',t:'𝙩',u:'𝙪',v:'𝙫',w:'𝙬',x:'𝙭',y:'𝙮',z:'𝙯'};
const MN={A:'𝙰',B:'𝙱',C:'𝙲',D:'𝙳',E:'𝙴',F:'𝙵',G:'𝙶',H:'𝙷',I:'𝙸',J:'𝙹',K:'𝙺',L:'𝙻',M:'𝙼',N:'𝙽',O:'𝙾',P:'𝙿',Q:'𝚀',R:'𝚁',S:'𝚂',T:'𝚃',U:'𝚄',V:'𝚅',W:'𝚆',X:'𝚇',Y:'𝚈',Z:'𝚉',a:'𝚊',b:'𝚋',c:'𝚌',d:'𝚍',e:'𝚎',f:'𝚏',g:'𝚐',h:'𝚑',i:'𝚒',j:'𝚓',k:'𝚔',l:'𝚕',m:'𝚖',n:'𝚗',o:'𝚘',p:'𝚙',q:'𝚚',r:'𝚛',s:'𝚜',t:'𝚝',u:'𝚞',v:'𝚟',w:'𝚠',x:'𝚡',y:'𝚢',z:'𝚣'};
function mp(s,m){return s.split('').map(c=>m[c]||c).join('');}
function applyInline(t){
t=t.replace(/\*\*(.+?)\*\*/g,(_,m)=>mp(m,B));
t=t.replace(/_(.+?)_/g,(_,m)=>mp(m,I));
return t;
}
function mapAll(t,m){
// map every char but preserve newlines & emojis
return t.split('').map(c=>m[c]||c).join('');
}
function stripMarkers(t){
return t.replace(/\*\*(.+?)\*\*/g,'$1').replace(/_(.+?)_/g,'$1');
}
// LinkedIn paragraph fix: blank lines get a zero-width space so they survive paste
function liParagraph(t){
return t.replace(/\n\n/g,'\n\u200b\n');
}
function buildVariants(raw){
const plain = stripMarkers(raw);
return [
{label:'Sans (Standard)', text: liParagraph(applyInline(raw))},
{label:'Bold Sans', text: liParagraph(mapAll(applyInline(raw),B))},
{label:'Italic Sans', text: liParagraph(mapAll(applyInline(raw),I))},
{label:'Bold Italic Sans', text: liParagraph(mapAll(applyInline(raw),BI))},
{label:'Monospace', text: liParagraph(mapAll(plain,MN))},
{label:'Sans (ohne Formatierung)',text: liParagraph(plain)},
];
}
let _variants=[];
function generate(){
const raw=document.getElementById('ta').value;
if(!raw.trim())return;
_variants=buildVariants(raw);
const right=document.getElementById('right');
right.innerHTML='<p class="hint">Variante wählen → <b>Copy text</b> → direkt in LinkedIn einfügen. Zeilenumbrüche bleiben erhalten.</p><div class="cards-grid" id="grid"></div>';
const grid=document.getElementById('grid');
_variants.forEach((v,i)=>{
const d=document.createElement('div');
d.className='variant-card';
d.innerHTML=`
<div class="variant-label">${v.label}</div>
<div class="li-post">
<div class="li-post-header">
<div class="li-avatar">JH</div>
<div><div class="li-name">Joachim Hummel</div><div class="li-sub">Senior IT Consultant · Automatisierung</div></div>
</div>
<div class="li-text" id="t${i}">${esc(v.text)}</div>
<div class="li-more" id="m${i}" onclick="exp(${i})" style="display:none">… mehr anzeigen</div>
</div>
<div class="card-foot">
<div class="li-actions">
<div class="li-act">👍 Like</div>
<div class="li-act">💬 Kommentar</div>
</div>
<button class="copy-btn" id="cb${i}" onclick="cp(${i})">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
Copy text
</button>
</div>`;
grid.appendChild(d);
setTimeout(()=>{
const el=document.getElementById('t'+i);
if(el&&el.scrollHeight>el.clientHeight+4){
const f=document.createElement('div');f.className='li-fade';el.appendChild(f);
document.getElementById('m'+i).style.display='block';
}
},60);
});
}
function exp(i){
const el=document.getElementById('t'+i);
el.classList.add('expanded');
const f=el.querySelector('.li-fade');if(f)f.remove();
document.getElementById('m'+i).style.display='none';
}
function cp(i){
const btn=document.getElementById('cb'+i);
const text=_variants[i].text;
if(navigator.clipboard&&window.isSecureContext){
navigator.clipboard.writeText(text).then(()=>flash(btn)).catch(()=>legacyCopy(text,btn));
} else {
legacyCopy(text,btn);
}
}
function legacyCopy(text,btn){
const ta=document.createElement('textarea');
ta.value=text;
ta.style.cssText='position:fixed;top:0;left:0;width:2px;height:2px;opacity:0.01;border:none;outline:none;';
document.body.appendChild(ta);
ta.focus();ta.select();ta.setSelectionRange(0,text.length);
try{ document.execCommand('copy'); flash(btn); }
catch(e){ alert('Kopieren fehlgeschlagen bitte manuell kopieren (Strg+A, Strg+C) aus dem Vorschau-Feld.'); }
document.body.removeChild(ta);
}
function flash(btn){
btn.textContent='✓ Kopiert!';btn.classList.add('copied');
setTimeout(()=>{
btn.innerHTML='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy text';
btn.classList.remove('copied');
},2000);
}
function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
document.getElementById('ta').addEventListener('input',()=>{
const len=document.getElementById('ta').value.length;
const el=document.getElementById('ctr');
const bar=document.getElementById('bar');
el.querySelector('strong').textContent=len.toLocaleString('de-DE');
const pct=Math.min(len/3000*100,100);
bar.style.width=pct+'%';
bar.style.background=pct>=100?'#c0392b':pct>=80?'#b45309':'#2d7a3a';
el.className='counter'+(pct>=100?' danger':pct>=80?' warn':'');
});
document.getElementById('ta').addEventListener('keydown',e=>{
if((e.ctrlKey||e.metaKey)&&e.key==='b'){e.preventDefault();wrapSel('**','**');}
if((e.ctrlKey||e.metaKey)&&e.key==='i'){e.preventDefault();wrapSel('_','_');}
if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){e.preventDefault();generate();}
});
function wrapSel(b,a){
const ta=document.getElementById('ta');
const s=ta.selectionStart,e=ta.selectionEnd,v=ta.value;
ta.value=v.slice(0,s)+b+v.slice(s,e)+a+v.slice(e);
ta.selectionStart=s+b.length;ta.selectionEnd=e+b.length;ta.focus();
}
function ins(ch){
const ta=document.getElementById('ta');
const s=ta.selectionStart,v=ta.value;
ta.value=v.slice(0,s)+ch+v.slice(s);
ta.selectionStart=ta.selectionEnd=s+ch.length;ta.focus();
}
function clearAll(){
document.getElementById('ta').value='';
document.getElementById('ctr').querySelector('strong').textContent='0';
document.getElementById('bar').style.width='0%';
document.getElementById('right').innerHTML='<div class="empty-state"><p>Text eingeben → „Vorschau" klicken</p><small>6 Schriftvarianten als LinkedIn-Karten — direkt per „Copy text" kopieren</small></div>';
}
</script>
</body>
</html>