From cc877b61216795d882af2f2ff87e13915e5a564b Mon Sep 17 00:00:00 2001 From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:28:02 +0200 Subject: [PATCH 01/37] Add Meme Battle Arena and expand archive to 150 memes Two cats enter, one cat leaves: head-to-head voting with streaks, win-rate leaderboard and local-only stats. Adds 90 caption remixes (60 to 150, 25 per mood) reusing the existing 24 templates, plus arena onboarding, keyboard/swipe voting and reset. --- public/app.js | 69 ++- public/index.html | 10 +- public/library.json | 1442 ++++++++++++++++++++++++++++++++++++++++++- public/style.css | 1 + 4 files changed, 1502 insertions(+), 20 deletions(-) diff --git a/public/app.js b/public/app.js index da1ec00..2a94072 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,5 @@ const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)]; -const paths={grid:'',dense:'',heart:'',folder:'',sparkles:'',shuffle:'',play:'',pause:'',keyboard:'',search:'',plus:'','arrow-right':'','arrow-left':'','arrow-down':'',x:'',download:'',upload:'',edit:'',link:'',check:''}; +const paths={grid:'',dense:'',trophy:'',heart:'',folder:'',sparkles:'',shuffle:'',play:'',pause:'',keyboard:'',search:'',plus:'','arrow-right':'','arrow-left':'','arrow-down':'',x:'',download:'',upload:'',edit:'',link:'',check:''}; const icon=n=>``; const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); function hydrateIcons(root=document){root.querySelectorAll('[data-icon]').forEach(e=>e.innerHTML=icon(e.dataset.icon));} @@ -10,6 +10,40 @@ const moodTag=m=>`${moodInfo(m)[1 const readState=(k,d)=>{try{return JSON.parse(localStorage.getItem('cmc-'+k))??d}catch{return d}}; const asIds=v=>new Set(Array.isArray(v)?v.filter(x=>typeof x==='string'):[]); let saved=asIds(readState('saved',[])),seen=asIds(readState('seen',[])),created=[],library=[],page='discover',mood='all',query='',collection='',limit=18,order=[]; +function readBattles(){const d=readState('battles',{wins:{},plays:{},battles:0,streak:0,best:0,lastWinner:null});return {wins:{...d.wins||{}},plays:{...d.plays||{}},battles:d.battles|0,streak:d.streak|0,best:d.best|0,lastWinner:typeof d.lastWinner==='string'?d.lastWinner:null}} +let battleStats=readBattles(),battleLeft=null,battleRight=null; +const battleWins=id=>battleStats.wins[id]|0; +const battlePlays=id=>battleStats.plays[id]|0; +const battleLosses=id=>Math.max(0,battlePlays(id)-battleWins(id)); +const battleRate=id=>{const p=battlePlays(id);return p?Math.round(battleWins(id)/p*100):null}; +function pickBattle(){const pool=allMemes();if(pool.length<2)return;let a=battleLeft,b=Math.random()<0.5?battleLeft:battleRight; + for(let i=0;i<20;i++){const x=pool[Math.floor(Math.random()*pool.length)],y=pool[Math.floor(Math.random()*pool.length)];if(x.id!==y.id&&(x.id!==a?.id||y.id!==b?.id)){a=x;b=y;break}} + if(!a||!b||a.id===b.id){const s=shuffle(pool);a=s[0];b=s[1]} + battleLeft=a;battleRight=b; +} +const battleTaunts=['The crowd goes mild.','A historic moment for cats.','Democracy works. No recount needed.','The chimp nods approvingly.','Questionable behavior, rewarded.','A landslide. Somehow still about naps.','The other cat demands a recount. Denied.','Instant classic. Frame it.','The judges (you) have spoken.','That one had main-character energy.','Science can’t explain this one.','Straight into the hall of fame.']; +function voteBattle(winnerId){if(!battleLeft||!battleRight)return; + battleStats.plays[battleLeft.id]=battlePlays(battleLeft.id)+1;battleStats.plays[battleRight.id]=battlePlays(battleRight.id)+1; + battleStats.wins[winnerId]=battleWins(winnerId)+1;battleStats.battles++;battleStats.streak++;battleStats.best=Math.max(battleStats.best,battleStats.streak);battleStats.lastWinner=winnerId; + setLocal('battles',battleStats); + const w=allMemes().find(m=>m.id===winnerId); + toast(`“${w?.title||'Cat'}” takes the crown. ${battleTaunts[Math.floor(Math.random()*battleTaunts.length)]}`); + pickBattle();renderArena(); +} +function skipBattle(){if(battleStats.streak>0)toast('Streak reset. The cats judge your indecision.');battleStats.streak=0;setLocal('battles',battleStats);pickBattle();renderArena()} +function resetArena(){if(!battleStats.battles){toast('No battles yet — crown a cat first.');return}if(!window.confirm('Reset your Arena record? Wins, streaks and the leaderboard go back to zero.'))return;battleStats={wins:{},plays:{},battles:0,streak:0,best:0,lastWinner:null};setLocal('battles',battleStats);pickBattle();renderArena();toast('Fresh slate. The cats forgive you.')} +function battleCard(m,side){const wins=battleWins(m.id),plays=battlePlays(m.id),rate=battleRate(m.id),label=moodInfo(m.mood)[2]; + return ``} +function renderArena(){if(!battleLeft||!battleRight)pickBattle();if(!battleLeft||!battleRight)return; + $('#arena-stats').innerHTML=`
${battleStats.battles}battles judged
${battleStats.streak} 🔥current streak
${battleStats.best} 🏆best streak
`; + const bc=$('#battle-count');if(bc)bc.textContent=battleStats.battles; + const last=battleStats.lastWinner?allMemes().find(m=>m.id===battleStats.lastWinner):null; + const lc=$('#last-crowned');if(last){lc.hidden=false;lc.innerHTML=`Last crowned: · ${battleWins(last.id)}W · ${battleRate(last.id)}%`}else{lc.hidden=true;lc.innerHTML=''} + $('#battle-grid').innerHTML=`${battleCard(battleLeft,'left')}${battleCard(battleRight,'right')}`; + const ranked=[...allMemes()].map(m=>({m,w:battleWins(m.id),p:battlePlays(m.id)})).filter(x=>x.p>0).sort((a,b)=>b.w-a.w||(b.w/b.p)-(a.w/a.p)||a.m.title.localeCompare(b.m.title)).slice(0,5); + const medals=['🥇','🥈','🥉','4.','5.']; + $('#leaderboard').innerHTML=`

Local legends

${battleStats.battles?`From ${battleStats.battles} judged ${battleStats.battles===1?'battle':'battles'} · stored only in this browser`:'No champions yet — your votes build this board. Stored only in this browser.'}

${ranked.length?ranked.map((x,i)=>{const r=Math.round(x.w/x.p*100);return `
${esc(x.m.title)}${x.w}W · ${x.p-x.w}L · ${r}%
`}).join(''):'

Crown your first cat above and it will show up here.

'}`; +} let viewerQueue=[],viewerIndex=0,autoTimer=null,toastTimer=null,editing=null,uploadedTemplate=null,dbPromise=null; const collections=[{id:'office',title:'The office survival kit',description:'For meetings that could have been naps.',image:'1bh7.jpg',filter:m=>m.tags.some(t=>['work','email','meeting','career','deadline'].includes(t))},{id:'battery',title:'The low battery club',description:'A safe space for professional nappers.',image:'11wis1.jpg',filter:m=>m.mood==='sleepy'},{id:'serotonin',title:'A little serotonin',description:'Small cats. Unreasonably big feelings.',image:'amuvy.jpg',filter:m=>m.mood==='wholesome'}]; function toast(message){clearTimeout(toastTimer);$('#toast').textContent=message;$('#toast').classList.add('visible');toastTimer=setTimeout(()=>$('#toast').classList.remove('visible'),3300);} @@ -28,15 +62,19 @@ function filterMemes(){let list=page==='studio'?[...created].reverse():page==='s } function visual(m,loading='lazy'){return `
${m.top?`
${esc(m.top)}
`:''}${esc(m.template)}${m.bottom?`
${esc(m.bottom)}
`:''}
`} function card(m,i){return `
${moodTag(m.mood)}${m.kind==='creation'?'YOUR CREATION':'CHIMP REMIX'}

${esc(m.title)}

`} -function updateCounts(){$('#all-count').textContent=library.length;$('#saved-count').textContent=allMemes().filter(m=>saved.has(m.id)).length;$('#created-count').textContent=created.length} +function updateCounts(){$('#all-count').textContent=library.length;$('#saved-count').textContent=allMemes().filter(m=>saved.has(m.id)).length;$('#created-count').textContent=created.length;const bc=$('#battle-count');if(bc)bc.textContent=battleStats.battles} function render(){updateCounts();$$('[data-page]').forEach(b=>{b.classList.toggle('active',b.dataset.page===page);if(b.dataset.page===page)b.setAttribute('aria-current','page');else b.removeAttribute('aria-current')}); - $('#hero').hidden=page!=='discover'||!!query; - $('#breadcrumb').textContent=({discover:'THE CAT ARCHIVE',saved:'SAVED FOR A RAINY DAY',collections:'HANDPICKED CAT COLLECTIONS',studio:'THE NONSENSE LAB'})[page]; - $('#section-eyebrow').textContent=({discover:'THE GOOD STUFF',saved:'YOUR PERSONAL SEROTONIN STASH',collections:'A CAT FOR EVERY OCCASION',studio:'MADE BY YOU. APPROVED BY YOU.'})[page]; - $('#library-title').textContent=query?`Cats matching “${query}”`:({discover:'Find your feline frequency.',saved:'The keepers.',collections:'Good things come in collections.',studio:'Your little masterpieces.'})[page]; - const desc=({discover:'',saved:'Every cat you’ve hearted, right here when you need one.',collections:'Small, handpicked corners of the archive. Pick one and settle in.',studio:'Your captions. Your cats. Your very questionable sense of humor.'})[page];$('#page-description').textContent=desc;$('#page-description').hidden=!desc; - $('#collection-grid').hidden=page!=='collections'; - if(page==='collections')$('#collection-grid').innerHTML=collections.map(c=>``).join(''); + $('#hero').hidden=page!=='discover'||!!query; + $('#breadcrumb').textContent=({discover:'THE CAT ARCHIVE',arena:'THE BATTLE ARENA',saved:'SAVED FOR A RAINY DAY',collections:'HANDPICKED CAT COLLECTIONS',studio:'THE NONSENSE LAB'})[page]||'THE CAT ARCHIVE'; + $('#section-eyebrow').textContent=({discover:'THE GOOD STUFF',arena:'TWO CATS ENTER. ONE CAT LEAVES.',saved:'YOUR PERSONAL SEROTONIN STASH',collections:'A CAT FOR EVERY OCCASION',studio:'MADE BY YOU. APPROVED BY YOU.'})[page]; + $('#library-title').textContent=page==='arena'?'Crown the funniest cat.':query?`Cats matching “${query}”`:({discover:'Find your feline frequency.',saved:'The keepers.',collections:'Good things come in collections.',studio:'Your little masterpieces.'})[page]; + const desc=({discover:'',arena:'150 memes. Zero mercy. Pick a winner, build a streak — skips reset the streak.',saved:'Every cat you’ve hearted, right here when you need one.',collections:'Small, handpicked corners of the archive. Pick one and settle in.',studio:'Your captions. Your cats. Your very questionable sense of humor.'})[page];$('#page-description').textContent=desc;$('#page-description').hidden=!desc; + const inArena=page==='arena'; + $('#arena').hidden=!inArena;$('#shuffle').hidden=inArena; + $('#filter-row').hidden=inArena;$('#collection-grid').hidden=page!=='collections'; + document.querySelector('.results-bar').hidden=inArena;$('#meme-grid').hidden=inArena;$('#empty').hidden=inArena?true:!!0;$('#load-more').hidden=inArena;$('#end-note').hidden=inArena; + if(inArena){if(!allMemes().length){$('#arena-stats').innerHTML='';$('#battle-grid').innerHTML='';$('#leaderboard').innerHTML='

The cats haven’t arrived yet.

'}else renderArena();return} + if(page==='collections')$('#collection-grid').innerHTML=collections.map(c=>``).join(''); $('#mood-filters').innerHTML=moods.map(([id,symbol,label])=>``).join(''); const list=filterMemes(),display=list.slice(0,limit);$('#meme-grid').innerHTML=display.map(card).join(''); $('#result-count').innerHTML=list.length?`${list.length} ${list.length===1?'cat':'cats'} ${page==='saved'?'worth keeping':mood==='all'&&!query&&!collection?'and not a single thought':collection?'in this collection':'in this corner'} · showing ${display.length}`:'0 cats found'; @@ -46,7 +84,7 @@ function render(){updateCounts();$$('[data-page]').forEach(b=>{b.classList.toggl $('#empty-copy').textContent=page==='saved'&&unfiltered?'Tap the heart on any meme. We’ll keep it warm for you.':page==='studio'&&unfiltered?'Pick a cat, add a caption, make someone’s day.':'Try a different search or let all the cats back in.'; $('#empty-action').textContent=page==='studio'&&unfiltered?'Make your first meme':page==='saved'&&unfiltered?'Find some favorites':'Show all cats'; } -function navigate(p){page=p;mood='all';query='';collection='';limit=18;$('#search').value='';render();window.scrollTo({top:0,behavior:'smooth'})} +function navigate(p){page=p;mood='all';query='';collection='';limit=18;$('#search').value='';if(p==='arena'&&(!battleLeft||!battleRight))pickBattle();render();window.scrollTo({top:0,behavior:'smooth'})} function toggleSave(id){const next=new Set(saved);next.has(id)?next.delete(id):next.add(id);if(!setLocal('saved',[...next]))return;saved=next; if(page==='saved')render();else{$$(`[data-save="${id}"]`).forEach(b=>{b.classList.toggle('saved',saved.has(id));b.setAttribute('aria-pressed',saved.has(id));const m=allMemes().find(m=>m.id===id);b.setAttribute('aria-label',`${saved.has(id)?'Unsave':'Save'} ${m?.title||'meme'}`)});updateCounts()} if($('#viewer').open)updateViewerSave();toast(saved.has(id)?'A good cat, safely tucked away.':'Released back into the wild.'); @@ -90,11 +128,11 @@ async function download(m,button){if(button)button.disabled=true;try{const blob= function info(title,content){$('#info-title').textContent=title;$('#info-content').innerHTML=content;$('#info').showModal()} function about(){info('An unserious archive. A serious color.',`

CATMEMECHIMP is a small, lovingly assembled corner of the internet for taking a cat break.

448 C · #4A412A
PERIWINKLE
WARM CREAM

The pixel chimp wears #4A412A, a screen approximation of Pantone 448 C. Deep olive-brown anchors the app; complementary blue and periwinkle bring a little lightness. Its silhouette comes from the canonical Chimp facekit.

Made with creative direction and code by GPT-6 Astra. No accounts, no subscription, no paid API, no tracking. Your favorites and creations stay in this browser.

Color reference ↗

`)} function sources(){info('A little credit for the cats.',`

The starter archive contains ${library.length} original CATMEMECHIMP caption remixes using ${templates().length} community cat templates from Imgflip. Every meme’s viewer links to its individual image source.

The captions were written for this app. Source photographs and templates belong to their respective creators; this app does not claim their ownership or grant reuse rights. Files are stored locally so the collection keeps working when you’re offline.

This is a curated collection, not a live trending feed. The starter collection was assembled on September 8, 2026.

Your favorites use browser storage and your creations use IndexedDB. Clearing this site’s browser data clears them; download your creations if you want a separate copy.

Explore the source templates on Imgflip ↗

`)} -function shortcuts(){info('Less clicking. More cats.',`
/Jump to search
RA random cat from this view
← / →Previous / next cat in the viewer
SSave the cat in the viewer
SpacePause / resume autoplay in the viewer
EscClose the current panel
?This little cheat sheet

On your phone, swipe left or right on a meme in the viewer. Autoplay changes cats every six seconds and pauses when you leave the tab.

`)} -document.addEventListener('click',e=>{const nav=e.target.closest('[data-page]');if(nav){navigate(nav.dataset.page);return}const open=e.target.closest('[data-open]');if(open){openViewer(open.dataset.open);return}const save=e.target.closest('[data-save]');if(save){toggleSave(save.dataset.save);return}const mb=e.target.closest('[data-mood]');if(mb?.classList.contains('mood-filter')){mood=mb.dataset.mood;limit=18;render();return}const cb=e.target.closest('[data-collection]');if(cb){collection=collection===cb.dataset.collection?'':cb.dataset.collection;mood='all';limit=18;render()}}); +function shortcuts(){info('Less clicking. More cats.',`
/Jump to search
RA random cat from this view
← / →Previous / next cat in the viewer, or vote in the Arena
SSave the cat in the viewer
SpacePause / resume autoplay in the viewer
EscClose the current panel
?This little cheat sheet

On your phone, swipe left or right on a meme in the viewer. Tap a cat to crown it in the Arena. Autoplay changes cats every six seconds and pauses when you leave the tab.

`)} +document.addEventListener('click',e=>{const nav=e.target.closest('[data-page]');if(nav){navigate(nav.dataset.page);return}const vt=e.target.closest('[data-vote]');if(vt){voteBattle(vt.dataset.vote);return}const open=e.target.closest('[data-open]');if(open){openViewer(open.dataset.open,page==='arena'?allMemes():filterMemes());return}const save=e.target.closest('[data-save]');if(save){toggleSave(save.dataset.save);return}const mb=e.target.closest('[data-mood]');if(mb?.classList.contains('mood-filter')){mood=mb.dataset.mood;limit=18;render();return}const cb=e.target.closest('[data-collection]');if(cb){collection=collection===cb.dataset.collection?'':cb.dataset.collection;mood='all';limit=18;render()}}); $('.brand').addEventListener('click',e=>{e.preventDefault();navigate('discover')}); $('#search').addEventListener('input',()=>{query=$('#search').value.trim();limit=18;render()});$('#sort').addEventListener('change',()=>{limit=18;render()}); -$('#shuffle').onclick=shuffledGrid;$('#hero-random').onclick=randomCat;$('#side-random').onclick=randomCat;$('#side-focus').onclick=()=>{const list=filterMemes();if(list.length)openViewer((list.find(m=>!seen.has(m.id))||list[0]).id);else toast('This corner needs a few cats first.')}; +$('#shuffle').onclick=shuffledGrid;$('#arena-skip').onclick=skipBattle;$('#arena-reset').onclick=resetArena;$('#hero-random').onclick=randomCat;$('#side-random').onclick=randomCat;$('#side-focus').onclick=()=>{const list=filterMemes();if(list.length)openViewer((list.find(m=>!seen.has(m.id))||list[0]).id);else toast('This corner needs a few cats first.')}; $('#load-more').onclick=()=>{const before=$$('#meme-grid .meme-card').length;limit+=18;render();const first=$$('#meme-grid .meme-open')[before];first?.focus({preventScroll:true})}; $('#back-top').onclick=()=>window.scrollTo({top:0,behavior:'smooth'}); $('#empty-action').onclick=()=>{if(page==='studio'&&!query&&mood==='all')openEditor();else navigate('discover')}; @@ -111,10 +149,11 @@ $('#about-open').onclick=about;$('#shortcuts-open').onclick=shortcuts;$('#source for(const d of $$('dialog'))d.addEventListener('click',e=>{if(e.target===d){const r=d.getBoundingClientRect();if(e.clientXr.right||e.clientYr.bottom)d.close()}}); document.addEventListener('keydown',e=>{if(e.ctrlKey||e.altKey||e.metaKey||/INPUT|TEXTAREA|SELECT/.test(e.target.tagName)||e.target.isContentEditable)return; if($('#viewer').open){if(e.key==='ArrowRight'){e.preventDefault();browse(1)}else if(e.key==='ArrowLeft'){e.preventDefault();browse(-1)}else if(e.key.toLowerCase()==='s'){e.preventDefault();toggleSave(currentMeme().id)}else if(e.code==='Space'){e.preventDefault();autoTimer?stopAutoplay():restartAutoplay()}return} - if($('dialog[open]'))return;if(e.key==='/'){e.preventDefault();$('#search').focus()}else if(e.key.toLowerCase()==='r'){e.preventDefault();randomCat()}else if(e.key==='?'){e.preventDefault();shortcuts()}}); + if($('dialog[open]'))return;if(e.key==='/'){e.preventDefault();$('#search').focus()}else if(e.key.toLowerCase()==='r'){e.preventDefault();randomCat()}else if(e.key==='?'){e.preventDefault();shortcuts()}else if(page==='arena'&&e.key==='ArrowLeft'&&battleLeft){e.preventDefault();voteBattle(battleLeft.id)}else if(page==='arena'&&e.key==='ArrowRight'&&battleRight){e.preventDefault();voteBattle(battleRight.id)}}); document.addEventListener('visibilitychange',()=>{if(document.hidden&&autoTimer)stopAutoplay()}); let swipe=null;$('#viewer-image-wrap').addEventListener('touchstart',e=>{swipe={x:e.changedTouches[0].clientX,y:e.changedTouches[0].clientY}},{passive:true});$('#viewer-image-wrap').addEventListener('touchend',e=>{if(!swipe)return;const dx=e.changedTouches[0].clientX-swipe.x,dy=e.changedTouches[0].clientY-swipe.y;if(Math.abs(dx)>55&&Math.abs(dx)>Math.abs(dy)*1.5)browse(dx<0?1:-1);swipe=null},{passive:true}); -async function init(){try{const response=await fetch('/library.json');if(!response.ok)throw new Error('The archive could not load.');library=await response.json();try{created=await dbRead()}catch{toast('Creations storage is unavailable; PNG downloads still work.')}render();const deepId=new URLSearchParams(location.hash.slice(1)).get('meme');if(deepId){const m=library.find(m=>m.id===deepId);if(m)openViewer(m.id,library);else toast('That cat link is not in this archive.')} +let battleSwipe=null;$('#battle-grid').addEventListener('touchstart',e=>{battleSwipe={x:e.changedTouches[0].clientX,y:e.changedTouches[0].clientY}},{passive:true});$('#battle-grid').addEventListener('touchend',e=>{if(!battleSwipe||page!=='arena')return;const dx=e.changedTouches[0].clientX-battleSwipe.x,dy=e.changedTouches[0].clientY-battleSwipe.y;battleSwipe=null;if(Math.abs(dx)>55&&Math.abs(dx)>Math.abs(dy)*1.5){if(dx<0&&battleRight)voteBattle(battleRight.id);else if(dx>0&&battleLeft)voteBattle(battleLeft.id)}},{passive:true}); +async function init(){try{const response=await fetch('/library.json');if(!response.ok)throw new Error('The archive could not load.');library=await response.json();try{created=await dbRead()}catch{toast('Creations storage is unavailable; PNG downloads still work.')}battleStats=readBattles();pickBattle();render();const deepId=new URLSearchParams(location.hash.slice(1)).get('meme');if(deepId){const m=library.find(m=>m.id===deepId);if(m)openViewer(m.id,library);else toast('That cat link is not in this archive.')} }catch(e){$('#result-count').textContent='The cats couldn’t arrive.';$('#empty').hidden=false;$('#empty-title').textContent='A small cat-astrophe.';$('#empty-copy').textContent='The local library could not load. Reload to try again.';$('#empty-action').textContent='Try again';$('#empty-action').onclick=()=>location.reload();console.error(e)}} window.addEventListener('hashchange',()=>{const id=new URLSearchParams(location.hash.slice(1)).get('meme');if(!id){if($('#viewer').open)closeViewer();return}const m=library.find(m=>m.id===id);if(!m){toast('That cat link is not in this archive.');return}if($('#viewer').open){viewerQueue=[...library];viewerIndex=viewerQueue.findIndex(m=>m.id===id);showViewer()}else openViewer(id,library)}); await init(); diff --git a/public/index.html b/public/index.html index bff6543..5c308a1 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - + CATMEMECHIMP — Good cats. Questionable behavior. @@ -14,7 +14,8 @@

Good cats.
Questionable behavior.

@@ -40,6 +40,7 @@
THE GOOD STUFF

Find your feline frequency.

+ @@ -53,7 +54,7 @@
A little less doom. A little more meow.
- +
THE CAT BREAK

Image source ↗ browse S save Esc close
THE NONSENSE LAB

Make a little mischief.

Saved in this browser. Yours to keep, download, and share.

diff --git a/public/style.css b/public/style.css index 3658405..107b844 100644 --- a/public/style.css +++ b/public/style.css @@ -50,6 +50,32 @@ .curse{font-weight:800;letter-spacing:.4px} [data-theme="dark"] .lab-intro{background:#252b45;border-color:#3a4370} @media(max-width:650px){.lab-layout{grid-template-columns:1fr}} +.pack-panel{display:grid;gap:12px;background:linear-gradient(135deg,var(--olive) 0%,#5c5136 100%);color:#f7f5ee;border-radius:13px;padding:22px;margin-bottom:16px;text-align:center} +.pack-sealed{background:none;border:2px dashed #f7f5ee66;border-radius:12px;padding:26px;display:grid;gap:6px;justify-items:center;color:#f7f5ee;width:100%} +.pack-sealed>span{font-size:64px} +.pack-sealed strong{font-size:18px;font-family:Fraunces,Georgia,serif} +.pack-sealed small{opacity:.75} +.pack-sealed:hover{border-color:#f7f5ee;background:#f7f5ee11;transform:translateY(-2px)} +.pack-sealed.opening>span{animation:packshake .5s ease-in-out infinite} +@keyframes packshake{0%,100%{transform:rotate(-8deg) scale(1)}50%{transform:rotate(8deg) scale(1.12)}} +.pack-wait{display:flex;gap:18px;align-items:center;justify-content:center;flex-wrap:wrap;text-align:left} +.pack-wait .arena-hint{color:#d7d1c0} +.pack-timer{font-family:Fraunces,Georgia,serif;font-size:30px} +.pack-mini{display:flex;gap:10px;align-items:center;background:#f7f5ee14;border:1px solid #f7f5ee33;border-radius:10px;padding:8px;text-align:left;color:#f7f5ee;max-width:340px} +.pack-mini img{width:64px;height:64px;object-fit:cover;border-radius:8px;flex-shrink:0} +.pack-mini small{opacity:.75;display:block} +.pack-mini.rare{border-color:#9fadf0;box-shadow:0 0 18px #9fadf066} +.pack-mini.legendary{border-color:#ffd75e;box-shadow:0 0 26px #ffd75e88} +.pack-mini.shiny{background:#ffd75e22} +.pack-mini.fresh{animation:packpop .45s ease-out} +@keyframes packpop{0%{transform:scale(.6);opacity:0}100%{transform:scale(1);opacity:1}} +.pack-tabs{display:flex;gap:8px;margin-bottom:14px} +.pack-tabs button{border:1px solid var(--line);background:var(--paper);border-radius:20px;padding:8px 14px;font-size:11px;font-weight:700} +.pack-tabs button.active{background:var(--olive);border-color:var(--olive);color:var(--paper)} +.meme-card{position:relative} +.shiny-badge{position:absolute;top:8px;left:8px;z-index:2;background:#ffd75e;color:#3a2f10;font-size:10px;font-weight:800;border-radius:6px;padding:3px 8px} +.meme-card.is-shiny{border-color:#d9c57c} +[data-theme="dark"] .pack-panel{background:linear-gradient(135deg,#2b2517,#3a3120);color:#f0e9d6} #stash{display:grid;gap:16px;margin-top:18px} .sets-panel,.trophies{background:var(--paper);border:1px solid var(--line);border-radius:13px;padding:16px} .trophy-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;margin-top:10px} diff --git a/scripts/smoke.mjs b/scripts/smoke.mjs index e3c68e3..d682af6 100644 --- a/scripts/smoke.mjs +++ b/scripts/smoke.mjs @@ -38,7 +38,7 @@ for (const m of js.matchAll(/\$\('#([A-Za-z0-9-]+)'\)/g)) { const count = html.match(/id="all-count">(\d+)/); if (!count) fail('index.html missing #all-count'); else if (Number(count[1]) !== lib.length) fail(`#all-count is ${count[1]} but library has ${lib.length}`); -for (const id of ['arena', 'battle-grid', 'arena-stats', 'leaderboard', 'last-crowned', 'arena-skip', 'arena-reset', 'lab', 'lab-preview', 'lab-provenance', 'lab-top', 'lab-bottom', 'lab-cat', 'lab-chaos', 'lab-save', 'lab-download', 'theme-toggle', 'sound-toggle', 'confetti', 'stash', 'sets-panel', 'trophies', 'viewer-collect', 'collect-dialog', 'collect-list', 'collect-new', 'collect-new-name', 'tv-toggle', 'tv-overlay', 'tv-chan', 'tv-clock', 'tv-live', 'tv-count', 'tv-name', 'tv-next', 'tv-ch-up', 'tv-ch-down', 'tv-vol', 'tv-speed', 'tv-sleep', 'tv-exit', 'screen', 'zap', 'meme-grid', 'viewer', 'editor']) { +for (const id of ['arena', 'battle-grid', 'arena-stats', 'leaderboard', 'last-crowned', 'arena-skip', 'arena-reset', 'lab', 'lab-preview', 'lab-provenance', 'lab-top', 'lab-bottom', 'lab-cat', 'lab-chaos', 'lab-save', 'lab-download', 'theme-toggle', 'sound-toggle', 'confetti', 'stash', 'sets-panel', 'trophies', 'viewer-collect', 'collect-dialog', 'collect-list', 'collect-new', 'collect-new-name', 'tv-toggle', 'tv-overlay', 'tv-chan', 'tv-clock', 'tv-live', 'tv-count', 'tv-name', 'tv-next', 'tv-ch-up', 'tv-ch-down', 'tv-vol', 'tv-speed', 'tv-sleep', 'tv-exit', 'screen', 'zap', 'packs', 'pack-panel', 'pack-stats', 'pack-tabs', 'haul-grid', 'daily-count', 'meme-grid', 'viewer', 'editor']) { if (!html.includes(`id="${id}"`)) fail(`index.html missing #${id}`); } for (const f of ['sw.js', 'app.js', 'style.css', 'library.json', 'manifest.webmanifest']) { From a68480370ef463095b1e9b06a8541a7a049d993b Mon Sep 17 00:00:00 2001 From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:37:52 +0200 Subject: [PATCH 17/37] Grow the archive to 250 memes --- public/app.js | 2 +- public/index.html | 4 +- public/library.json | 1600 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1603 insertions(+), 3 deletions(-) diff --git a/public/app.js b/public/app.js index ad73ff7..d8eb0f0 100644 --- a/public/app.js +++ b/public/app.js @@ -168,7 +168,7 @@ function render(){updateCounts();$$('[data-page]').forEach(b=>{b.classList.toggl $('#breadcrumb').textContent=({discover:'THE CAT ARCHIVE',arena:'THE BATTLE ARENA',lab:'THE FRANKENMEME LAB',packs:'THE DAILY DROP',saved:'SAVED FOR A RAINY DAY',collections:'HANDPICKED CAT COLLECTIONS',studio:'THE NONSENSE LAB'})[page]||'THE CAT ARCHIVE'; $('#section-eyebrow').textContent=({discover:'THE GOOD STUFF',arena:'TWO CATS ENTER. ONE CAT LEAVES.',lab:'MAD SCIENCE, BUT CATS.',packs:'A LITTLE GAMBLE.',saved:'YOUR PERSONAL SEROTONIN STASH',collections:'A CAT FOR EVERY OCCASION',studio:'MADE BY YOU. APPROVED BY YOU.'})[page]; $('#library-title').textContent=page==='arena'?'Crown the funniest cat.':page==='lab'?'Build an abomination.':page==='packs'?"Today's drop.":query?`Cats matching “${query}”`:({discover:'Find your feline frequency.',saved:'The keepers.',collections:'Good things come in collections.',studio:'Your little masterpieces.'})[page]; - const desc=({discover:'',arena:'150 memes. Zero mercy. Pick a winner, build a streak — skips reset the streak.',lab:'Three cats walk in. One meme walks out. Reroll parts until it feels illegal.',packs:'One sealed pack per human per 24 hours. Rip it, keep the cat, chase the streak.',saved:'Every cat you’ve hearted, right here when you need one.',collections:'Small, handpicked corners of the archive. Pick one and settle in.',studio:'Your captions. Your cats. Your very questionable sense of humor.'})[page];$('#page-description').textContent=desc;$('#page-description').hidden=!desc; + const desc=({discover:'',arena:'250 memes. Zero mercy. Pick a winner, build a streak — skips reset the streak.',lab:'Three cats walk in. One meme walks out. Reroll parts until it feels illegal.',packs:'One sealed pack per human per 24 hours. Rip it, keep the cat, chase the streak.',saved:'Every cat you’ve hearted, right here when you need one.',collections:'Small, handpicked corners of the archive. Pick one and settle in.',studio:'Your captions. Your cats. Your very questionable sense of humor.'})[page];$('#page-description').textContent=desc;$('#page-description').hidden=!desc; const inArena=page==='arena',inLab=page==='lab',inPacks=page==='packs',hideGrid=inArena||inLab||inPacks; $('#arena').hidden=!inArena;$('#lab').hidden=!inLab;$('#packs').hidden=!inPacks;$('#shuffle').hidden=hideGrid; $('#filter-row').hidden=hideGrid;$('#collection-grid').hidden=page!=='collections'; diff --git a/public/index.html b/public/index.html index 78c925e..351a593 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - + CATMEMECHIMP — Good cats. Questionable behavior. @@ -14,7 +14,7 @@

Good cats.
Questionable behavior.