Initial commit
This commit is contained in:
288
public/index.html
Normal file
288
public/index.html
Normal 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> / 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,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user