From a20f8eb015a0bd493083dae222f75ea6ca2c3842 Mon Sep 17 00:00:00 2001 From: Krzysztof Rudnicki Date: Mon, 8 Sep 2025 12:06:31 +0200 Subject: [PATCH] feat: added author info --- articles/index.html | 28 +++++++++++++--------------- articles/server_c.c | 34 +++++++++++++++++----------------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/articles/index.html b/articles/index.html index 1053099..e33d799 100644 --- a/articles/index.html +++ b/articles/index.html @@ -29,7 +29,7 @@ article h1{margin:.25rem 0 .5rem;font-size:1.6rem} .spinner{width:16px;height:16px;border:2px solid #ccc;border-top-color:#333;border-radius:50%;display:inline-block;animation:spin 1s linear infinite} @keyframes spin{to{transform:rotate(360deg)}} .reading header nav{display:none} -.reading #delBtn,.reading #dateInfo{display:none} +.reading #delBtn{display:none}
@@ -60,8 +60,11 @@ article h1{margin:.25rem 0 .5rem;font-size:1.6rem} + + + -
Drop image here or choose…
+
Choose image…
@@ -88,11 +91,12 @@ article h1{margin:.25rem 0 .5rem;font-size:1.6rem} const loading=$('#loading'); const rT=$('#readTitle'), rB=$('#readBody'), rTh=$('#readThumb'), dt=$('#dateInfo'); const toList=$('#toList'), toNew=$('#toNew'), back=$('#backBtn'), del=$('#delBtn'); - const title=$('#title'), content=$('#content'); + const title=$('#title'), author=$('#author'), content=$('#content'); const tDrop=$('#thumbDrop'), tPrev=$('#thumbPrev'), tFile=$('#thumbFile'); const insImg=$('#insImg'), insVid=$('#insVid'), imgFile=$('#imgFile'), vidFile=$('#vidFile'); const saveBtn=$('#saveBtn'), cancelBtn=$('#cancelBtn'); const KEY='articles14k'; + const PH="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 180 120'%3E%3Crect width='100%25' height='100%25' fill='%23eee'/%3E%3C/svg%3E"; let selId=null; let API=null; let preList; // cache first list to avoid duplicate initial fetches const AC={}; function setLoading(on,msg){ if(!loading) return; if(msg) loading.querySelector('.msg').textContent=msg; loading.classList.toggle('hidden',!on); } @@ -150,7 +154,7 @@ article h1{margin:.25rem 0 .5rem;font-size:1.6rem} finally{ setLoading(false); } } if(!a.length){listV.innerHTML='
No articles yet. Click New to create one.
';return} - listV.innerHTML=a.map(x=>`
thumb

${esc(x.title)}

`).join(''); + listV.innerHTML=a.map(x=>`
thumb

${esc(x.title)}

`).join(''); } function esc(s){return (s||'').replace(/[&<>]/g,m=>({"&":"&","<":"<",">":">"}[m]))} @@ -159,14 +163,12 @@ article h1{margin:.25rem 0 .5rem;font-size:1.6rem} function fileToDataURL(f){return new Promise((res,rej)=>{const r=new FileReader(); r.onload=()=>res(r.result); r.onerror=rej; r.readAsDataURL(f)})} function dataURLToBlob(dataURL){ try{ const [h,b]=dataURL.split(','); const m=h.match(/data:([^;]+)(;base64)?/); if(!m) return null; const mime=m[1]; const isB64=!!m[2]; const raw=isB64? atob(b): decodeURIComponent(b); const arr=new Uint8Array(raw.length); for(let i=0;i{e.preventDefault(); zone.style.background='#f7f7f7'}); zone.addEventListener('dragleave',()=>zone.style.background=''); zone.addEventListener('drop',e=>{e.preventDefault(); zone.style.background=''; const fs=[...(e.dataTransfer.files||[])]; fs.forEach(cb)});} - // Thumb drag/drop or file select - dropHandler(tDrop, f=>{ if(!f.type.startsWith('image/')) return; fileToDataURL(f).then(u=>{tPrev.src=u})}); + // Thumbnail via file chooser only tFile.addEventListener('change',e=>{const f=e.target.files[0]; if(f) fileToDataURL(f).then(u=>tPrev.src=u)}); tDrop.addEventListener('click',()=>tFile.click()); - // Content drag/drop insert and paste + // Content paste (keep), no drag & drop content.addEventListener('paste',e=>{const it=(e.clipboardData||{}).items||[]; for(const x of it){const f=x.getAsFile&&x.getAsFile(); if(!f) continue; e.preventDefault(); if(f.type.startsWith('image/')) { // Insert a placeholder while uploading const ph=imgEl(''); ph.alt='image'; ph.style.minHeight='120px'; ph.style.background='#f0f0f0'; insertAtSel(ph); @@ -174,10 +176,6 @@ article h1{margin:.25rem 0 .5rem;font-size:1.6rem} } else if(f.type.startsWith('video/')) { fileToDataURL(f).then(u=>{const v=videoEl(u); insertAtSel(v)}); } } }); - dropHandler(content, f=>{ if(f.type.startsWith('image/')) { - const ph=imgEl(''); ph.alt='image'; ph.style.minHeight='120px'; ph.style.background='#f0f0f0'; insertAtSel(ph); - (async()=>{ const url=await uploadBlobAndGetURL(f); if(url){ ph.removeAttribute('style'); ph.src=url; } else { fileToDataURL(f).then(u=>{ ph.removeAttribute('style'); ph.src=u; }); } })(); - } else if(f.type.startsWith('video/')) { fileToDataURL(f).then(u=>{const v=videoEl(u); insertAtSel(v)}) } }); // Insert buttons insImg.onclick=()=>{ imgFile.onchange=async e=>{ const f=e.target.files&&e.target.files[0]; if(!f) return; const ph=imgEl(''); ph.alt='image'; ph.style.minHeight='120px'; ph.style.background='#f0f0f0'; insertAtSel(ph); const url=await uploadBlobAndGetURL(f); if(url){ ph.removeAttribute('style'); ph.src=url; } else { fileToDataURL(f).then(u=>{ ph.removeAttribute('style'); ph.src=u; }); } }; imgFile.click(); }; @@ -190,7 +188,7 @@ article h1{margin:.25rem 0 .5rem;font-size:1.6rem} let th=tPrev.src||''; if(!th){const fi=content.querySelector('img'); if(fi) th=fi.src} // If thumbnail is data URL, try uploading to separate file so it loads separately if(th && th.startsWith('data:')){ const blob=dataURLToBlob(th); if(blob){ const u=await uploadBlobAndGetURL(blob); if(u) th=u; } } - const obj={title:title.value.trim()||'Untitled', body:content.innerHTML, thumb:th||''}; + const obj={title:title.value.trim()||'Untitled', author:author.value.trim(), body:content.innerHTML, thumb:th||''}; setLoading(true,'Saving…'); await API.create(obj); selId=null; clearEditor(); @@ -199,12 +197,12 @@ article h1{margin:.25rem 0 .5rem;font-size:1.6rem} show(listV); }; - function clearEditor(){ title.value=''; content.innerHTML=''; tPrev.removeAttribute('src') } + function clearEditor(){ title.value=''; author.value=''; content.innerHTML=''; tPrev.removeAttribute('src') } cancelBtn.onclick=()=>{selId=null; clearEditor(); show(listV)}; // List click -> read (ensure full focus by switching view) listV.addEventListener('click',async e=>{const card=e.target.closest('.card'); if(!card) return; const id=card.getAttribute('data-id'); const c=AC[id]; if(c){ openRead(c); return; } setLoading(true,'Loading article…'); try{ const a=await API.get(id); if(!a) return; AC[id]=a; openRead(a); } finally { setLoading(false); }}); - function openRead(a){ selId=a.id; rTh.loading='lazy'; rTh.decoding='async'; rTh.src=a.thumb||''; rT.textContent=a.title||''; rB.innerHTML=a.body||''; rB.querySelectorAll('img').forEach(im=>{ im.loading='lazy'; im.decoding='async'; }); dt.textContent=new Date(a.createdAt||Date.now()).toLocaleString(); show(readV) } + function openRead(a){ selId=a.id; rTh.loading='lazy'; rTh.decoding='async'; rTh.src=a.thumb||''; rT.textContent=a.title||''; rB.innerHTML=a.body||''; rB.querySelectorAll('img').forEach(im=>{ im.loading='lazy'; im.decoding='async'; }); const d=new Date(a.createdAt||Date.now()).toLocaleString(); dt.textContent=(a.author? a.author+' · ': '')+d; show(readV) } // Delete del.onclick=async()=>{ if(!selId) return; setLoading(true,'Deleting…'); await API.remove(selId); delete AC[selId]; selId=null; await renderList(); setLoading(false); show(listV) }; diff --git a/articles/server_c.c b/articles/server_c.c index 6e775f3..85bf592 100644 --- a/articles/server_c.c +++ b/articles/server_c.c @@ -112,16 +112,16 @@ static char* json_get_top_string(const char* json, const char* key){ } // Build object JSON string; caller frees -static char* build_article_json(const char* id, const char* title, const char* body, const char* thumb, long long createdAt, long long updatedAt){ - char *et=json_escape(title?title:""), *eb=json_escape(body?body:""), *eth=json_escape(thumb?thumb:""); - if(!et||!eb||!eth){ free(et); free(eb); free(eth); return NULL; } +static char* build_article_json(const char* id, const char* title, const char* author, const char* body, const char* thumb, long long createdAt, long long updatedAt){ + char *et=json_escape(title?title:""), *eau=json_escape(author?author:""), *eb=json_escape(body?body:""), *eth=json_escape(thumb?thumb:""); + if(!et||!eau||!eb||!eth){ free(et); free(eau); free(eb); free(eth); return NULL; } char createdBuf[64]; snprintf(createdBuf,sizeof(createdBuf),"%lld",createdAt); char updated[96]=""; if(updatedAt>0){ snprintf(updated,sizeof(updated),",\"updatedAt\":%lld",updatedAt); } - size_t need = strlen(id)+strlen(et)+strlen(eb)+strlen(eth)+strlen(createdBuf)+strlen(updated)+64; + size_t need = strlen(id)+strlen(et)+strlen(eau)+strlen(eb)+strlen(eth)+strlen(createdBuf)+strlen(updated)+80; char* out=malloc(need); - if(!out){ free(et); free(eb); free(eth); return NULL; } - snprintf(out, need, "{\"id\":\"%s\",\"title\":\"%s\",\"body\":\"%s\",\"thumb\":\"%s\",\"createdAt\":%s%s}", id, et, eb, eth, createdBuf, updated); - free(et); free(eb); free(eth); return out; + if(!out){ free(et); free(eau); free(eb); free(eth); return NULL; } + snprintf(out, need, "{\"id\":\"%s\",\"title\":\"%s\",\"author\":\"%s\",\"body\":\"%s\",\"thumb\":\"%s\",\"createdAt\":%s%s}", id, et, eau, eb, eth, createdBuf, updated); + free(et); free(eau); free(eb); free(eth); return out; } static char* gen_id(){ char* out=malloc(17); if(!out) return NULL; unsigned int r = (unsigned int)rand(); long long t=now_ms(); snprintf(out,17,"%08x%08x", (unsigned int)(t&0xffffffff), r); return out; } @@ -215,7 +215,7 @@ static int rewrite_articles_map(char** out_json_updated, const char* match_id, c // collect objects size_t len=strlen(t); size_t i=1; int depth=0; size_t start=0; size_t count=0, cap=8; char** objs=malloc(cap*sizeof(char*)); bool found=false; char* updated_copy=NULL; for(; ibl+1){ const char* id=path+bl+1; char* obj=find_article_by_id(id); if(!obj){ send_response(c,404,"Not Found","application/json","",0,true); return;} // migrate this object if needed - char* title=json_get_string(obj, "title"); char* body_s=json_get_string(obj, "body"); char* thumb=json_get_string(obj, "thumb"); long long createdAt=json_get_number(obj, "\"createdAt\""); int obj_changed=0; + char* title=json_get_string(obj, "title"); char* author=json_get_string(obj, "author"); char* body_s=json_get_string(obj, "body"); char* thumb=json_get_string(obj, "thumb"); long long createdAt=json_get_number(obj, "\"createdAt\""); int obj_changed=0; if(thumb && strncmp(thumb, "data:",5)==0){ char* mime=NULL; unsigned char* bytes=NULL; size_t bl2=0; if(parse_data_url(thumb,&mime,&bytes,&bl2)==0){ const char* ext=ext_from_mime(mime); char* saved=save_bytes_with_ext(bytes,bl2,ext); if(saved){ free(thumb); size_t urlL=strlen(saved)+2; thumb=malloc(urlL); snprintf(thumb,urlL,"/%s",saved); free(saved); obj_changed=1; } free(mime); free(bytes); } } bool bchanged=false; char* new_body=migrate_inline_images_in_body(body_s,&bchanged); if(new_body && bchanged){ free(body_s); body_s=new_body; obj_changed=1; } else { free(new_body); } - if(obj_changed){ char* id_copy=json_get_string(obj, "id"); char* updated=build_article_json(id_copy?id_copy:"", title?title:"", body_s?body_s:"", thumb?thumb:"", createdAt, 0); // persist + if(obj_changed){ char* id_copy=json_get_string(obj, "id"); char* updated=build_article_json(id_copy?id_copy:"", title?title:"", author?author:"", body_s?body_s:"", thumb?thumb:"", createdAt, 0); // persist if(updated){ rewrite_articles_map(NULL, id_copy, updated, false); free(updated); } free(id_copy); - free(obj); obj=build_article_json(json_get_string("",""), title?title:"", body_s?body_s:"", thumb?thumb:"", createdAt, 0); // rebuild for response + free(obj); obj=build_article_json(json_get_string("",""), title?title:"", author?author:"", body_s?body_s:"", thumb?thumb:"", createdAt, 0); // rebuild for response // Above line uses placeholder; simpler: rebuild from previously updated string parsing is heavy; instead send the updated we built // But rewrite_articles_map returned updated; safer to just re-find free(obj); obj=find_article_by_id(id); } - free(title); free(body_s); free(thumb); + free(title); free(author); free(body_s); free(thumb); size_t L=strlen(obj); send_response(c,200,"OK","application/json",obj,L,true); free(obj); return; } } else if(!strcmp(method,"POST") && !strcmp(path, base)){