Loading editor/widgets/googlereviews/googlereviews.cpp +145 −48 Original line number Diff line number Diff line Loading @@ -64,58 +64,155 @@ namespace blogi { void Settings(libhttppp::HttpRequest &curreq, libhtmlpp::HtmlElement &settings) override {} void Render(libhtmlpp::HtmlElement &el) const override { // Wrapper div for reviews std::string uid = "gr_" + std::string(Id); libhtmlpp::HtmlElement *container = new libhtmlpp::HtmlElement("div"); container->setAttribute("id", "googlereviews-container"); container->setAttribute("class", "googlereviews-grid"); container->setAttribute("id", uid.c_str()); el.appendChild(container); // Add script to dynamically load reviews libhtmlpp::HtmlElement *script = new libhtmlpp::HtmlElement("script"); libhtmlpp::ScriptElement js; js.setScript(R"( document.addEventListener('DOMContentLoaded', function() { fetch('/google/reviews/list') .then(response => response.json()) .then(data => { const container = document.getElementById('googlereviews-container'); if (!data || data.length === 0) { container.innerHTML = '<p>Noch keine Bewertungen vorhanden.</p>'; return; js.setScript(R"JS( (function(){ var cs = document.currentScript; var root = cs ? cs.previousElementSibling : null; if(!root) return; var uid = root.id; if(!document.getElementById(uid+'_css')){ var st = document.createElement('style'); st.id = uid+'_css'; st.textContent = '#'+uid+'{background:#fff;border-radius:16px;box-shadow:0 18px 45px rgba(15,23,42,.12),0 0 0 1px rgba(15,23,42,.03);padding:32px 24px;box-sizing:border-box;width:100%;font-family:inherit;}'+ '#'+uid+' .gr-row{display:flex;align-items:flex-start;gap:24px;}'+ '#'+uid+' .gr-hdr{width:220px;flex-shrink:0;display:flex;flex-direction:column;align-items:center;text-align:center;padding-top:12px;}'+ '#'+uid+' .gr-logo{height:44px;margin-bottom:8px;}'+ '#'+uid+' .gr-avg{font-size:2.2em;font-weight:700;color:#222;line-height:1;}'+ '#'+uid+' .gr-avgstars{color:#ffb400;font-size:1.15em;margin:4px 0 2px;}'+ '#'+uid+' .gr-cnt{font-size:.82em;color:#888;}'+ '#'+uid+' .gr-wrap{position:relative;flex:1;overflow:hidden;}'+ '#'+uid+' .gr-vp{overflow:hidden;}'+ '#'+uid+' .gr-track{display:flex;transition:transform .4s ease;gap:16px;}'+ '#'+uid+' .gr-card{flex:0 0 calc(33.333% - 11px);max-width:calc(33.333% - 11px);background:#f9fafb;border-radius:14px;padding:16px;display:flex;flex-direction:row;gap:12px;border:1px solid rgba(148,163,184,.3);box-sizing:border-box;}'+ '#'+uid+' .gr-av{width:40px;height:40px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:1.05em;color:#fff;}'+ '#'+uid+' .gr-bd{flex:1;font-size:.95em;color:#111827;}'+ '#'+uid+' .gr-name{font-weight:600;margin-bottom:3px;}'+ '#'+uid+' .gr-rstars{color:#fbbf24;font-size:.9em;margin-bottom:4px;}'+ '#'+uid+' .gr-txt{font-size:.92em;line-height:1.45;}'+ '#'+uid+' .gr-dt{font-size:.79em;color:#6b7280;margin-top:6px;}'+ '#'+uid+' .gr-btn{position:absolute;top:50%;transform:translateY(-50%);width:32px;height:32px;border-radius:50%;border:1px solid #d1d5db;background:#fff;cursor:pointer;box-shadow:0 2px 6px rgba(15,23,42,.15);z-index:10;font-size:1em;display:flex;align-items:center;justify-content:center;padding:0;}'+ '#'+uid+' .gr-btn:disabled{opacity:.35;cursor:default;}'+ '#'+uid+' .gr-prev{left:-6px;}'+ '#'+uid+' .gr-next{right:-6px;}'+ '#'+uid+' .gr-more{border:0;background:none;padding:0;color:#2563eb;cursor:pointer;font-size:.9em;}'+ '@media(max-width:768px){'+ '#'+uid+'{padding:24px 16px;}'+ '#'+uid+' .gr-row{flex-direction:column;}'+ '#'+uid+' .gr-hdr{width:100%;}'+ '#'+uid+' .gr-card{flex:0 0 100%;max-width:100%;}'+ '}'; document.head.appendChild(st); } var AVCOLORS = ['#4f46e5','#0891b2','#059669','#d97706','#dc2626','#7c3aed','#db2777','#0284c7']; function avColor(name){ var h=0; for(var i=0;i<name.length;i++) h=(h*31+name.charCodeAt(i))&0xffff; return AVCOLORS[h%AVCOLORS.length]; } function starsHtml(n){ var s=''; for(var i=0;i<5;i++) s+=(i<n)?'★':'☆'; return s; } container.innerHTML = ''; data.forEach(rev => { let stars = ''; for(let i=0; i<5; i++) { if (i < rev.rating) stars += '⭐'; else stars += '★'; function formatDate(ts){ if(!ts) return ''; var n=Number(ts); var d=n>1e9 ? new Date(n*1000) : new Date(ts); return isNaN(d.getTime()) ? '' : d.toLocaleDateString('de-DE'); } function perPage(){ return window.innerWidth<=768?1:3; } const box = document.createElement('div'); box.style = 'background-color:#ffffff; padding:20px; border-radius:10px; border:1px solid #d4e8a8; margin-bottom:20px; flex:1; min-width:250px;'; function build(reviews){ if(!reviews||!reviews.length){ root.textContent='Noch keine Bewertungen vorhanden.'; return; } var sum=0; for(var i=0;i<reviews.length;i++) sum+=reviews[i].rating||0; var avg=Math.round((sum/reviews.length)*10)/10; root.innerHTML= '<div class="gr-row">'+ '<div class="gr-hdr">'+ '<img class="gr-logo" src="https://www.gstatic.com/images/branding/googlelogo/svg/googlelogo_clr_74x24px.svg" alt="Google">'+ '<div class="gr-avg">'+avg.toFixed(1)+'</div>'+ '<div class="gr-avgstars">'+starsHtml(Math.round(avg))+'</div>'+ '<div class="gr-cnt">'+reviews.length+' Bewertungen</div>'+ '</div>'+ '<div class="gr-wrap">'+ '<button type="button" class="gr-btn gr-prev" aria-label="Zurück">❮</button>'+ '<div class="gr-vp"><div class="gr-track"></div></div>'+ '<button type="button" class="gr-btn gr-next" aria-label="Weiter">❯</button>'+ '</div>'+ '</div>'; var track=root.querySelector('.gr-track'); reviews.forEach(function(r){ var name=r.author_name||'Anonym'; var init=name.charAt(0).toUpperCase(); var full=String(r.text||''); var isLong=full.length>100; var short=isLong?full.slice(0,100)+'…':full; var card=document.createElement('div'); card.className='gr-card'; card.innerHTML= '<div class="gr-av" style="background:'+avColor(name)+'">'+init+'</div>'+ '<div class="gr-bd" data-full="'+encodeURIComponent(full)+'">'+ '<div class="gr-name">'+name+'</div>'+ '<div class="gr-rstars">'+starsHtml(r.rating||5)+'</div>'+ '<div class="gr-txt">'+short+(isLong?' <button type="button" class="gr-more">mehr lesen</button>':'')+'</div>'+ '<div class="gr-dt">'+formatDate(r.time)+'</div>'+ '</div>'; track.appendChild(card); }); const head = document.createElement('div'); head.style = 'font-size:14px; color:#07071a; font-weight:bold; padding-bottom:10px;'; head.innerText = rev.author_name + ' ' + stars; var page=0; var pp=perPage(); function total(){ return Math.max(1,Math.ceil(reviews.length/pp)); } function slide(idx){ idx=Math.max(0,Math.min(idx,total()-1)); var first=track.children[0]; var w=first?(first.offsetWidth+16):0; track.style.transform='translateX(-'+(idx*pp*w)+'px)'; page=idx; } const body = document.createElement('div'); body.style = 'font-size:14px; color:#3d4a30;'; body.innerText = '"' + rev.text + '"'; var prev=root.querySelector('.gr-prev'); var next=root.querySelector('.gr-next'); if(prev) prev.addEventListener('click',function(){ slide(page<=0?total()-1:page-1); }); if(next) next.addEventListener('click',function(){ slide(page>=total()-1?0:page+1); }); box.appendChild(head); box.appendChild(body); container.appendChild(box); }); }) .catch(err => console.error('Error fetching reviews:', err)); track.addEventListener('click',function(ev){ if(!ev.target.classList.contains('gr-more')) return; var bd=ev.target.closest('.gr-bd'); if(bd) bd.querySelector('.gr-txt').textContent=decodeURIComponent(bd.getAttribute('data-full')||''); }); )"); script->appendChild(js); // Add inline style for a responsive flex grid wrapper if needed container->setAttribute("style", "display: flex; gap: 30px; flex-wrap: wrap; justify-content: center;"); window.addEventListener('resize',function(){ pp=perPage(); slide(0); }); slide(0); } el.appendChild(container); fetch('/google/reviews/list') .then(function(r){ if(!r.ok) throw new Error(r.status); return r.json(); }) .then(build) .catch(function(e){ console.error('GoogleReviews widget:', e); root.textContent='Bewertungen konnten nicht geladen werden.'; }); })(); )JS"); script->appendChild(js); el.appendChild(script); } Loading Loading
editor/widgets/googlereviews/googlereviews.cpp +145 −48 Original line number Diff line number Diff line Loading @@ -64,58 +64,155 @@ namespace blogi { void Settings(libhttppp::HttpRequest &curreq, libhtmlpp::HtmlElement &settings) override {} void Render(libhtmlpp::HtmlElement &el) const override { // Wrapper div for reviews std::string uid = "gr_" + std::string(Id); libhtmlpp::HtmlElement *container = new libhtmlpp::HtmlElement("div"); container->setAttribute("id", "googlereviews-container"); container->setAttribute("class", "googlereviews-grid"); container->setAttribute("id", uid.c_str()); el.appendChild(container); // Add script to dynamically load reviews libhtmlpp::HtmlElement *script = new libhtmlpp::HtmlElement("script"); libhtmlpp::ScriptElement js; js.setScript(R"( document.addEventListener('DOMContentLoaded', function() { fetch('/google/reviews/list') .then(response => response.json()) .then(data => { const container = document.getElementById('googlereviews-container'); if (!data || data.length === 0) { container.innerHTML = '<p>Noch keine Bewertungen vorhanden.</p>'; return; js.setScript(R"JS( (function(){ var cs = document.currentScript; var root = cs ? cs.previousElementSibling : null; if(!root) return; var uid = root.id; if(!document.getElementById(uid+'_css')){ var st = document.createElement('style'); st.id = uid+'_css'; st.textContent = '#'+uid+'{background:#fff;border-radius:16px;box-shadow:0 18px 45px rgba(15,23,42,.12),0 0 0 1px rgba(15,23,42,.03);padding:32px 24px;box-sizing:border-box;width:100%;font-family:inherit;}'+ '#'+uid+' .gr-row{display:flex;align-items:flex-start;gap:24px;}'+ '#'+uid+' .gr-hdr{width:220px;flex-shrink:0;display:flex;flex-direction:column;align-items:center;text-align:center;padding-top:12px;}'+ '#'+uid+' .gr-logo{height:44px;margin-bottom:8px;}'+ '#'+uid+' .gr-avg{font-size:2.2em;font-weight:700;color:#222;line-height:1;}'+ '#'+uid+' .gr-avgstars{color:#ffb400;font-size:1.15em;margin:4px 0 2px;}'+ '#'+uid+' .gr-cnt{font-size:.82em;color:#888;}'+ '#'+uid+' .gr-wrap{position:relative;flex:1;overflow:hidden;}'+ '#'+uid+' .gr-vp{overflow:hidden;}'+ '#'+uid+' .gr-track{display:flex;transition:transform .4s ease;gap:16px;}'+ '#'+uid+' .gr-card{flex:0 0 calc(33.333% - 11px);max-width:calc(33.333% - 11px);background:#f9fafb;border-radius:14px;padding:16px;display:flex;flex-direction:row;gap:12px;border:1px solid rgba(148,163,184,.3);box-sizing:border-box;}'+ '#'+uid+' .gr-av{width:40px;height:40px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:1.05em;color:#fff;}'+ '#'+uid+' .gr-bd{flex:1;font-size:.95em;color:#111827;}'+ '#'+uid+' .gr-name{font-weight:600;margin-bottom:3px;}'+ '#'+uid+' .gr-rstars{color:#fbbf24;font-size:.9em;margin-bottom:4px;}'+ '#'+uid+' .gr-txt{font-size:.92em;line-height:1.45;}'+ '#'+uid+' .gr-dt{font-size:.79em;color:#6b7280;margin-top:6px;}'+ '#'+uid+' .gr-btn{position:absolute;top:50%;transform:translateY(-50%);width:32px;height:32px;border-radius:50%;border:1px solid #d1d5db;background:#fff;cursor:pointer;box-shadow:0 2px 6px rgba(15,23,42,.15);z-index:10;font-size:1em;display:flex;align-items:center;justify-content:center;padding:0;}'+ '#'+uid+' .gr-btn:disabled{opacity:.35;cursor:default;}'+ '#'+uid+' .gr-prev{left:-6px;}'+ '#'+uid+' .gr-next{right:-6px;}'+ '#'+uid+' .gr-more{border:0;background:none;padding:0;color:#2563eb;cursor:pointer;font-size:.9em;}'+ '@media(max-width:768px){'+ '#'+uid+'{padding:24px 16px;}'+ '#'+uid+' .gr-row{flex-direction:column;}'+ '#'+uid+' .gr-hdr{width:100%;}'+ '#'+uid+' .gr-card{flex:0 0 100%;max-width:100%;}'+ '}'; document.head.appendChild(st); } var AVCOLORS = ['#4f46e5','#0891b2','#059669','#d97706','#dc2626','#7c3aed','#db2777','#0284c7']; function avColor(name){ var h=0; for(var i=0;i<name.length;i++) h=(h*31+name.charCodeAt(i))&0xffff; return AVCOLORS[h%AVCOLORS.length]; } function starsHtml(n){ var s=''; for(var i=0;i<5;i++) s+=(i<n)?'★':'☆'; return s; } container.innerHTML = ''; data.forEach(rev => { let stars = ''; for(let i=0; i<5; i++) { if (i < rev.rating) stars += '⭐'; else stars += '★'; function formatDate(ts){ if(!ts) return ''; var n=Number(ts); var d=n>1e9 ? new Date(n*1000) : new Date(ts); return isNaN(d.getTime()) ? '' : d.toLocaleDateString('de-DE'); } function perPage(){ return window.innerWidth<=768?1:3; } const box = document.createElement('div'); box.style = 'background-color:#ffffff; padding:20px; border-radius:10px; border:1px solid #d4e8a8; margin-bottom:20px; flex:1; min-width:250px;'; function build(reviews){ if(!reviews||!reviews.length){ root.textContent='Noch keine Bewertungen vorhanden.'; return; } var sum=0; for(var i=0;i<reviews.length;i++) sum+=reviews[i].rating||0; var avg=Math.round((sum/reviews.length)*10)/10; root.innerHTML= '<div class="gr-row">'+ '<div class="gr-hdr">'+ '<img class="gr-logo" src="https://www.gstatic.com/images/branding/googlelogo/svg/googlelogo_clr_74x24px.svg" alt="Google">'+ '<div class="gr-avg">'+avg.toFixed(1)+'</div>'+ '<div class="gr-avgstars">'+starsHtml(Math.round(avg))+'</div>'+ '<div class="gr-cnt">'+reviews.length+' Bewertungen</div>'+ '</div>'+ '<div class="gr-wrap">'+ '<button type="button" class="gr-btn gr-prev" aria-label="Zurück">❮</button>'+ '<div class="gr-vp"><div class="gr-track"></div></div>'+ '<button type="button" class="gr-btn gr-next" aria-label="Weiter">❯</button>'+ '</div>'+ '</div>'; var track=root.querySelector('.gr-track'); reviews.forEach(function(r){ var name=r.author_name||'Anonym'; var init=name.charAt(0).toUpperCase(); var full=String(r.text||''); var isLong=full.length>100; var short=isLong?full.slice(0,100)+'…':full; var card=document.createElement('div'); card.className='gr-card'; card.innerHTML= '<div class="gr-av" style="background:'+avColor(name)+'">'+init+'</div>'+ '<div class="gr-bd" data-full="'+encodeURIComponent(full)+'">'+ '<div class="gr-name">'+name+'</div>'+ '<div class="gr-rstars">'+starsHtml(r.rating||5)+'</div>'+ '<div class="gr-txt">'+short+(isLong?' <button type="button" class="gr-more">mehr lesen</button>':'')+'</div>'+ '<div class="gr-dt">'+formatDate(r.time)+'</div>'+ '</div>'; track.appendChild(card); }); const head = document.createElement('div'); head.style = 'font-size:14px; color:#07071a; font-weight:bold; padding-bottom:10px;'; head.innerText = rev.author_name + ' ' + stars; var page=0; var pp=perPage(); function total(){ return Math.max(1,Math.ceil(reviews.length/pp)); } function slide(idx){ idx=Math.max(0,Math.min(idx,total()-1)); var first=track.children[0]; var w=first?(first.offsetWidth+16):0; track.style.transform='translateX(-'+(idx*pp*w)+'px)'; page=idx; } const body = document.createElement('div'); body.style = 'font-size:14px; color:#3d4a30;'; body.innerText = '"' + rev.text + '"'; var prev=root.querySelector('.gr-prev'); var next=root.querySelector('.gr-next'); if(prev) prev.addEventListener('click',function(){ slide(page<=0?total()-1:page-1); }); if(next) next.addEventListener('click',function(){ slide(page>=total()-1?0:page+1); }); box.appendChild(head); box.appendChild(body); container.appendChild(box); }); }) .catch(err => console.error('Error fetching reviews:', err)); track.addEventListener('click',function(ev){ if(!ev.target.classList.contains('gr-more')) return; var bd=ev.target.closest('.gr-bd'); if(bd) bd.querySelector('.gr-txt').textContent=decodeURIComponent(bd.getAttribute('data-full')||''); }); )"); script->appendChild(js); // Add inline style for a responsive flex grid wrapper if needed container->setAttribute("style", "display: flex; gap: 30px; flex-wrap: wrap; justify-content: center;"); window.addEventListener('resize',function(){ pp=perPage(); slide(0); }); slide(0); } el.appendChild(container); fetch('/google/reviews/list') .then(function(r){ if(!r.ok) throw new Error(r.status); return r.json(); }) .then(build) .catch(function(e){ console.error('GoogleReviews widget:', e); root.textContent='Bewertungen konnten nicht geladen werden.'; }); })(); )JS"); script->appendChild(js); el.appendChild(script); } Loading