mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 18:43:08 +02:00
feat: added author info
This commit is contained in:
parent
80d7fee6f9
commit
a20f8eb015
@ -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}
|
||||
</style>
|
||||
|
||||
<header>
|
||||
@ -60,8 +60,11 @@ article h1{margin:.25rem 0 .5rem;font-size:1.6rem}
|
||||
<label>Title</label>
|
||||
<input id="title" type="text" placeholder="Article title"/>
|
||||
|
||||
<label>Author</label>
|
||||
<input id="author" type="text" placeholder="Author (optional)"/>
|
||||
|
||||
<label>Thumbnail</label>
|
||||
<div id="thumbDrop" class="drop">Drop image here or choose…<br><img id="thumbPrev"/></div>
|
||||
<div id="thumbDrop" class="drop">Choose image…<br><img id="thumbPrev"/></div>
|
||||
<input id="thumbFile" type="file"/>
|
||||
|
||||
<label>Body</label>
|
||||
@ -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='<div class="small">No articles yet. Click New to create one.</div>';return}
|
||||
listV.innerHTML=a.map(x=>`<div class="card" data-id="${x.id}"><img loading="lazy" decoding="async" src="${x.thumb||''}" alt="thumb"><h3>${esc(x.title)}</h3></div>`).join('');
|
||||
listV.innerHTML=a.map(x=>`<div class="card" data-id="${x.id}"><img loading="lazy" decoding="async" src="${x.thumb||PH}" alt="thumb"><h3>${esc(x.title)}</h3></div>`).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<raw.length;i++) arr[i]=raw.charCodeAt(i); return new Blob([arr], {type:mime}); }catch(e){ return null; } }
|
||||
function insertAtSel(node){const s=window.getSelection(); if(!s||!s.rangeCount){content.appendChild(node); return} const r=s.getRangeAt(0); r.deleteContents(); r.insertNode(node); r.setStartAfter(node); r.setEndAfter(node); s.removeAllRanges(); s.addRange(r)}
|
||||
function dropHandler(zone, cb){zone.addEventListener('dragover',e=>{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) };
|
||||
|
||||
@ -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(; i<len; ++i){ char c=t[i]; if(c=='{'){ if(depth==0) start=i; depth++; } else if(c=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
|
||||
char* id=json_get_string(obj, "id"); bool isMatch=id && strcmp(id,match_id)==0; if(isMatch){ found=true; if(!is_delete){ char* title=json_get_string(obj, "title"); char* body=json_get_string(obj, "body"); char* thumb=json_get_string(obj, "thumb"); char* ptitle=json_get_top_string(patch_json, "title"); if(ptitle&&*ptitle){ free(title); title=ptitle; } else free(ptitle); char* pbody=json_get_top_string(patch_json, "body"); if(pbody&&*pbody){ free(body); body=pbody; } else free(pbody); char* pthumb=json_get_top_string(patch_json, "thumb"); if(pthumb&&*pthumb){ free(thumb); thumb=pthumb; } else free(pthumb); long long createdAt=json_get_number(obj, "\"createdAt\""); long long updatedAt=now_ms(); char* obj2=build_article_json(id,title,body,thumb,createdAt,updatedAt); free(title); free(body); free(thumb);
|
||||
char* id=json_get_string(obj, "id"); bool isMatch=id && strcmp(id,match_id)==0; if(isMatch){ found=true; if(!is_delete){ char* title=json_get_string(obj, "title"); char* author=json_get_string(obj, "author"); char* body=json_get_string(obj, "body"); char* thumb=json_get_string(obj, "thumb"); char* ptitle=json_get_top_string(patch_json, "title"); if(ptitle&&*ptitle){ free(title); title=ptitle; } else free(ptitle); char* pauthor=json_get_top_string(patch_json, "author"); if(pauthor&&*pauthor){ free(author); author=pauthor; } else free(pauthor); char* pbody=json_get_top_string(patch_json, "body"); if(pbody&&*pbody){ free(body); body=pbody; } else free(pbody); char* pthumb=json_get_top_string(patch_json, "thumb"); if(pthumb&&*pthumb){ free(thumb); thumb=pthumb; } else free(pthumb); long long createdAt=json_get_number(obj, "\"createdAt\""); long long updatedAt=now_ms(); char* obj2=build_article_json(id,title,author,body,thumb,createdAt,updatedAt); free(title); free(author); free(body); free(thumb);
|
||||
free(updated_copy); updated_copy=strdup(obj2); free(obj); obj=obj2; }
|
||||
}
|
||||
free(id);
|
||||
@ -238,8 +238,8 @@ static int rewrite_articles_map(char** out_json_updated, const char* match_id, c
|
||||
}
|
||||
|
||||
static char* create_article_from_body(const char* body_json){
|
||||
char* title=json_get_top_string(body_json, "title"); char* b=json_get_top_string(body_json, "body"); char* th=json_get_top_string(body_json, "thumb"); char* id=gen_id(); long long t=now_ms(); char* obj=build_article_json(id,title,b,th,t,0);
|
||||
free(title); free(b); free(th); if(!id||!obj){ free(id); free(obj); return NULL; }
|
||||
char* title=json_get_top_string(body_json, "title"); char* author=json_get_top_string(body_json, "author"); char* b=json_get_top_string(body_json, "body"); char* th=json_get_top_string(body_json, "thumb"); char* id=gen_id(); long long t=now_ms(); char* obj=build_article_json(id,title,author,b,th,t,0);
|
||||
free(title); free(author); free(b); free(th); if(!id||!obj){ free(id); free(obj); return NULL; }
|
||||
char* file=data_file(); if(!file){ free(id); free(obj); return NULL; }
|
||||
size_t n=0; char* content=read_file_all(file,&n);
|
||||
if(!content || n==0){ // write new array
|
||||
@ -291,15 +291,15 @@ static void handle_api(int c, const char* method, const char* path, const char*
|
||||
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*)); int changed=0;
|
||||
for(; i<len; ++i){ char ch=t[i]; if(ch=='{'){ if(depth==0) start=i; depth++; } else if(ch=='}'){ depth--; if(depth==0){ size_t end=i; size_t obj_len=end-start+1; char* obj=malloc(obj_len+1); memcpy(obj, t+start, obj_len); obj[obj_len]='\0';
|
||||
// check thumb
|
||||
char* id=json_get_string(obj, "id"); 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\"");
|
||||
char* id=json_get_string(obj, "id"); 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;
|
||||
// migrate thumb if data URL
|
||||
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); }
|
||||
}
|
||||
// migrate inline images in body
|
||||
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){ changed=1; free(obj); char* obj2=build_article_json(id?id:"",title?title:"",body_s?body_s:"",thumb?thumb:"",createdAt,0); obj=obj2; }
|
||||
free(id); free(title); free(body_s); free(thumb);
|
||||
if(obj_changed){ changed=1; free(obj); char* obj2=build_article_json(id?id:"",title?title:"",author?author:"",body_s?body_s:"",thumb?thumb:"",createdAt,0); obj=obj2; }
|
||||
free(id); free(title); free(author); free(body_s); free(thumb);
|
||||
if(count==cap){ cap*=2; objs=realloc(objs, cap*sizeof(char*)); }
|
||||
objs[count++]=obj;
|
||||
}
|
||||
@ -314,18 +314,18 @@ static void handle_api(int c, const char* method, const char* path, const char*
|
||||
} else if(path[bl]=='/' && strlen(path)>bl+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)){
|
||||
|
||||
Loading…
Reference in New Issue
Block a user