Repository: pveyes/katla Branch: main Commit: a5195dfc488c Files: 69 Total size: 176.7 KB Directory structure: gitextract_1iyklgqu/ ├── .eslintrc.json ├── .github/ │ └── workflows/ │ ├── test.yml │ └── update.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .prettierignore ├── .scripts/ │ ├── answers.csv │ ├── update.js │ └── whitelist.csv ├── .vscode/ │ └── settings.json ├── README.md ├── components/ │ ├── Alert.tsx │ ├── App.tsx │ ├── Board.tsx │ ├── Container.tsx │ ├── EmojiRain.tsx │ ├── EmojiSelector.tsx │ ├── Header.tsx │ ├── HeadingWithNum.tsx │ ├── HelpModal.tsx │ ├── Keyboard.tsx │ ├── KeyboardButton.tsx │ ├── Link.tsx │ ├── LiveStatsModal.tsx │ ├── Modal.tsx │ ├── SettingsModal.tsx │ ├── SponsorshipFooter.tsx │ ├── StatsModal.tsx │ └── Tile.tsx ├── env.dev ├── jest.config.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages/ │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.js │ ├── api/ │ │ ├── define/ │ │ │ └── [word].ts │ │ ├── live.ts │ │ └── words.ts │ ├── arsip/ │ │ ├── [num].tsx │ │ └── index.tsx │ ├── bantuan.tsx │ ├── index.tsx │ └── lawan.tsx ├── postcss.config.js ├── public/ │ ├── ads.txt │ └── google563f40b6a67a6d75.html ├── sentry.client.config.js ├── sentry.properties ├── sentry.server.config.js ├── styles/ │ └── globals.css ├── tailwind.config.js ├── tsconfig.json └── utils/ ├── __tests__/ │ ├── answer.test.js │ └── game.test.js ├── animation.ts ├── answers.ts ├── browser.ts ├── codec.ts ├── constants.ts ├── fetcher.ts ├── formatter.ts ├── game.ts ├── liveGame.ts ├── message.ts ├── tracking.ts ├── types.ts └── useStoredState.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "next/core-web-vitals" } ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: push: branches: [main] pull_request: branches: [main] jobs: test: if: "!contains(github.event.head_commit.message, '[skip ci]')" runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js 14.x uses: actions/setup-node@v1 with: node-version: "14.x" - name: Install & test run: | yarn install --pure-lockfile --prefer-offline yarn test ================================================ FILE: .github/workflows/update.yml ================================================ on: schedule: # TODO: use new zealand time? # Update on every 00:00 GMT+9 / WIT which means 15:00 UTC # Also sync timezone change to update.js/TIMEZONE_OFFSET - cron: "00 15 * * *" jobs: update_katla: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: node-version: "14.x" registry-url: "https://registry.npmjs.org" - name: install deps run: yarn install --prefer-offline - name: update new word run: yarn update env: SECRET_DATE: ${{ secrets.SECRET_DATE }} SECRET_WORD: ${{ secrets.SECRET_WORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env.local .env.development.local .env.test.local .env.production.local .scripts/wordle.json .scripts/nyt.json .scripts/analysis.js # vercel .vercel # Sentry .sentryclirc ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" npx lint-staged ================================================ FILE: .prettierignore ================================================ build coverage .next .vercel ================================================ FILE: .scripts/answers.csv ================================================ ganar,pakar,syair,ultah,danur,wisma,ruang,tajin,saksi,palsu,skrin,jeruk,vokal,belok,acian,jenuh,prabu,rahim,eceng,bocah,anyir,robek,bison,gesit,jepit,debus,beruk,porsi,uapan,sabtu,polan,parau,hukum,lawan,setan,butir,makan,rafia,virus,gemuk,comel,lobak,globe,angin,bujur,morse,jumat,undak,belit,keong,hasut,bolos,zebra,irama,rogoh,butuh,virga,waduk,strok,ideal,domba,emoji,dekor,turun,tinja,premi,nadim,kayuh,jujur,sekon,kroco,jeluk,dakwa,kodok,gipsi,volan,frasa,eksak,rompi,tulis,ranah,sadis,angka,surga,lumer,didik,ancai,cuman,yolks,sorot,aksen,garda,ulser,alang,suami,sihir,tekuk,cerai,luang,biner,seduh,hewan,jerih,iklan,caing,gimik,gumam,julur,cecak,pasar,sabar,puyuh,jujut,jiran,april,klise,imbuh,biara,darah,guyur,joker,dekil,zuhur,lerai,mambu,gusar,minum,tutor,gusur,rambu,gojek,pohon,pisau,kueri,kunci,mudah,cacah,cimol,hiena,rumor,kokok,gebuk,sipir,ceruk,duvet,versi,nuget,sepuh,bokeh,takar,andai,luber,apaan,nomor,stres,lepak,lembu,pedal,lebah,porus,cuaca,senin,alibi,berat,ngung,indra,endus,nafsu,kiara,kemon,bunyi,wajah,serah,memar,waras,tentu,lorek,bakul,mudik,folio,lalap,idiot,kimia,curam,licik,baang,cawan,dalih,uwete,tatap,satwa,licin,pasif,wesel,gagal,lotre,jambu,canda,bewok,skrub,celup,debak,resek,ampai,kumur,khiar,lahan,tipis,bihun,penad,jerat,diari,sidik,jurus,satai,kawat,cegah,besan,loket,motif,bibel,mebel,apron,penyu,kakak,luluh,kanda,jejak,radar,desau,nikah,sayup,gowok,jalan,pokok,jilid,obral,bisul,pelek,teguk,jibun,garpu,yakni,perlu,sohib,bahan,culun,sunda,bekel,singa,surau,lelet,patri,susun,datau,orang,didih,gokar,judul,napas,kibor,totol,rakyu,ralat,fatwa,tanah,yakin,batas,rakus,zombi,mahar,istri,batin,manja,bobin,lugas,tukul,serat,absen,impun,windu,kambu,konte,masif,derit,tusuk,pergi,ceker,vasal,cebol,legam,cedok,diksi,teras,kamar,rawon,bidai,andil,putus,gubuk,caima,sapun,pause,belut,kikuk,harta,cagar,mukim,punca,berak,rumus,ompok,kalke,tabur,puyer,urgen,pelik,tuduh,esais,latah,vadem,lotak,hantu,ndoro,gotik,putih,riset,daing,titis,tepuk,genre,pupus,tulus,perut,getas,kalap,cihui,delik,intim,injil,gelar,jarum,tabel,udang,jurik,saleh,gincu,gerha,nihil,jebol,adang,kikis,kebab,latif,derai,sesak,pelit,humor,avgas,bacin,semai,paras,cibuk,cerih,jihad,iring,tante,akrab,ultah,imsak,hotel,union,tikus,kotor,detik,serbu,zumba,kanal,hilal,kubis,makar,tarif,birai,trans,sisik,sukar,retro,freon,jersi,kasui,tahan,cocok,biksu,advis,ihwal,valas,jakun,mafia,cadas,daeng,setel,rasis,firma,gawai,beras,wajib,kelor,timun,kurma,rival,ludes,tajam,lodeh,wulan,krans,cabai,pelor,teteh,polis,heboh,massa,restu,orbit,makna,bongo,valid,tawon,pukul,konde,abjad,salvo,belia,opera,usang,nenek,rancu,benak,jubin,bobol,iblis,hutan,vonis,absah,honje,sinus,datar,darat,emosi,suara,culas,lusuh,cikar,duafa,jimat,nisan,larva,karya,bantu,unjuk,wahyu,coket,salju,lahap,kukuh,kring,benur,siswa,modem,acara,suatu,siaga,sawah,panas,siapa,afair,geger,kecoh,ritme,banda,gawat,warga,kuaci,karat,ovasi,lumut,cetai,lomba,kiper,lever,plang,cekah,kotak,buron,kenyi,pilah,porto,hidup,punya,lelap,sakau,prosa,kakap,lisan,pasak,rukun,semok,untuk,celus,kerja,tebak,retas,korps,ialah,jemur,cerek,terka,udara,swike,bufet,gapai,jumpa,cebur,lurik,cakar,infak,maksi,kemih,imaji,hilir,sasis,bonus,sains,jomlo,senja,bujet,ambai,ejaan,fitur,trofi,biasa,perak,bazar,pecuk,kenap,spasi,subuh,sisir,kukus,vital,lawak,umrah,tomat,katak,pivot,pahat,eboni,betis,tumit,jinak,kapak,nista,helai,tabib,mesti,semen,kribo,logat,resin,visum,uskup,garis,kakus,besit,segar,debum,impor,egois,norma,plano,makro,surya,bubur,insan,ikrar,imbal,rudal,jingu,pulen,kreol,feral,pilih,senil,turut,guyub,fondu,damar,derap,untai,terpa,intan,kawin,ciduk,spons,tifus,hindu,lampu,retak,bedak,jidur,sadar,kromo,judes,ombak,wasit,lahar,jompo,titip,skors,wafat,murid,dekap,paket,badai,opini,jijik,mesra,kapal,indah,tetua,himne,detap,mandi,tidur,urban,imlek,kerap,insaf,tanpa,tutup,oasis,botia,rebah,benua,titik,bruto,gelas,sandi,magma,avtur,tabuh,tinju,ilusi,sinar,savan,bulir,benih,wadah,jemah,tetap,ciput,warna,gagah,gravi,langu,antek,susah,inang,timur,jewer,melek,rilis,cengo,tajir,zakat,demam,perih,lotus,tokek,jawat,pekik,ivori,tensi,nyiur,teori,blitz,hiruk,dahak,agama,dogma,bebop,empuk,nasib,candu,dorbi,liwet,sebum,karst,kover,organ,kekar,tanya,cecer,kemam,siram,slang,cerun,ceria,nanas,kucur,kanan,anani,keris,kedok,balai,urung,habis,bahwa,jawil,junub,halus,keruk,gawar,mania,sumba,encok,tetas,liter,peang,kekal,minim,rubah,amboi,faali,ultah,busik,fosil,siswi,pecat,rimba,gulai,tabik,putri,jegil,jotos,cekik,hawar,wafer,limfa,acuan,cepuk,kulit,danau,yogia,pipih,pecah,takwa,rawan,kuyup,ledek,glosa,punuk,pedas,patah,puasa,orasi,nyawa,lusin,batal,tutur,rouks,buyar,objek,fokus,murka,deguk,cekok,tulat,telat,elips,butut,rindu,tiara,laksa,kroma,bidik,bezit,rugbi,decak,yatim,kunut,runut,sikat,tegel,diska,leleh,pipis,datin,burik,gawir,sanca,kenop,capai,ceter,oktaf,sorak,canai,mogok,tempe,antre,muara,razia,tanda,nyali,rahib,camat,bakda,perca,cadar,jajan,damai,fusia,tiang,kutub,ironi,atlet,ngilu,serta,iuran,payau,netra,kekat,bedil,momok,lelah,wangi,bunda,cadai,kawah,bigot,tiada,tugas,waria,liang,risau,rezim,janda,jumbo,lihai,citra,teror,drama,ceeng,ketat,joget,kurva,vakum,ujung,vista,faksi,jimbe,luwes,andal,khaki,cetus,samar,topik,pejal,telah,taoco,kefir,idiom,lalat,tatah,trafo,ihram,pakan,umami,novel,denim,syekh,ontel,niaga,kucek,fiber,beton,ulang,sause,penat,bakat,yahya,omong,empat,bioma,yonko,pejam,talak,copet,mesem,buaya,gigih,baiat,tanam,oknum,beres,jahat,subur,putra,elung,joglo,menor,buyut,sepah,bugis,fobia,kucup,flora,cacak,sonde,paksa,ranji,animo,wakaf,aktif,semut,bisai,wagyu,besut,koran,comek,admin,siang,merah,rutin,kepak,mantu,gurau,tuyul,wajan,gadis,tekor,cogan,selak,kameo,satin,gebos,sipil,jerit,rokok,seeng,cemas,tewas,susuk,stepa,sedih,boleh,tubir,koala,sibuk,ronde,menit,buani,curai,bodoh,saraf,senar,cerna,tunda,bambu,kinan,iftar,emisi,jenis,kucir,belah,rumit,embun,steno,pompa,taala,bivak,cadel,plong,kerak,siger,hirup,madya,upeti,celah,bagai,tubin,sling,wajik,sosor,kalis,tempa,nyata,cubit,unyil,bulai,rayap,kawal,jelek,gatal,busai,totok,jaksa,angan,novum,setop,tenis,cebok,alaan,taksi,letih,meses,iseng,barai,halwa,bakau,tamat,gempa,behel,kawuk,tatak,dawai,klien,sendu,dahan,tafia,sigma,pines,ajaib,silau,cekal,puisi,nakal,kelus,kawak,selir,gamis,skala,kuasa,janin,ceguk,buket,kepul,panji,jreng,bajik,payah,moral,bleng,becak,kawan,fasad,hujat,tesis,viral,mesiu,merek,sirna,colek,kidal,sahur,lazim,kekau,entok,suaka,panda,cekup,pedis,fardu,kedam,derus,vinil,arsip,namun,nasal,gajih,dower,jodoh,tolan,jawab,cling,beban,cukai,desis,sawer,sayat,oktet,enzim,intip,lepas,paraf,jatuh,warta,intro,setia,ultah,piano,sikap,sanda,pasir,fakta,ukhti,punah,lukis,binar,tiris,terus,entri,datum,sipit,pasal,botol,horas,envoi,pesek,felon,buruh,beguk,mawar,vespa,ujian,salat,rumah,kumuh,pulau,dewan,oliva,usung,nyala,jajah,pedih,onsen,makam,korve,klaim,ricuh,koplo,afiks,tikar,seken,sinis,urine,abaya,abate,bayar,dasar,fasia,gaduh,dassi,erosi,elusi,fatal,fiksi,fungi,demen,encot,gacor,tambi,ruing,awang,medio,murni,betet,keram,laten,sekat,kader,sulam,ligat,tenda,gugur,atase,medik,rusak,disel,kupat,papar,human,japan,ekses,mohon,rebut,kurau,stasi,graha,jamil,sebut,skrip,muram,becik,gerak,makin,bunuh,areal,kirab,untun,kafir,kelab,resan,harga,tegur,kubur,foton,salam,cukur,akmal,asing,nambi,ampar,pakal,figur,pusar,syura,tegak,islam,ngeri,tunik,almon,kecak,asasi,omzet,nazar,bekas,fadil,eksis,sabah,badui,jetis,tapis,kedua,colok,kania,telak,seisi,curah,umbar,masuk,grasi,kopra,fasis,jajal,geber,sisil,detak,lebak,jambe,salut,wukuf,tambo,gajah,cicil,juang,unsur,paruh,lalai,eyang,dekat,kayak,ramen,duduk,inses,alpin,bulus,palak,karel,amino,binal,seruk,angit,kejut,kupas,kuang,duhai,jamur,pihak,kudus,kasia,keluh,pasti,solid,tujuh,keran,rawat,ampel,jatah,kodak,badik,hoaks,crash,pegan,lapar,puspa,yusuf,sabda,natal,afdal,kamus,ranau,ishak,rekap,gabah,metro,gulir,bajak,seksi,rusuk,demak,sukuk,dendi,diare,eweai,kuria,cilik,studi,hadis,sidat,basis,ilham,pikat,loker,jalal,tekat,kipas,sejuk,lapuk,talun,rakit,ketua,bayur,wajar,papan,torsi,hafiz,bacok,sulit,bumbu,roker,jegog,china,acang,tanak,level,randi,donor,talam,ritel,subam,kenek,ganda,curat,gosip,petuk,legit,titan,entah,solok,kabir,pakai,jahit,bahar,bugar,griya,manah,fjord,bulan,buluh,isian,lapis,final,amsal,barak,cacar,induk,gatra,anime,reksa,fulan,tonga,infus,ahang,angel,angsa,cukup,redah,bagan,teuku,butik,nusuk,badau,pagar,gudeg,lemon,kalau,biola,ranti,sowan,lemas,kesah,sakar,utama,copot,pakis,sabuk,abung,zalim,lahab,suwir,tebal,maman,dayak,piton,musik,jasad,bisik,manta,pucuk,karim,perah,asesi,etana,aroma,imbas,lebur,dumbo,dunia,pamit,bedah,gamat,adobe,honor,sikka,bagus,rapuh,walau,tilas,linen,cicit,adika,bakam,tanur,sigap,baret,batuk,sanad,sudah,pulut,serba,penda,apnea,kusam,rabat,patil,lunak,saran,tania,oncom,kelik,ultah,pikir,ajoli,hanif,optik,kriya,pulih,dulur,bahau,lolak,tulip,nipis,gobak,magis,cemar,mitos,aswad,kasur,porno,medan,silam,kumis,utang,obeng,abang,taksu,pinta,pepah,mandu,momen,wujud,surut,coban,cempe,pamah,macet,orion,ludah,dalem,polip,sirah,fakir,tupai,rekor,bahas,madam,setek,ribut,ampat,sudan,cikal,hisab,kicau,komik,semak,viola,kedap,sogan,tukas,tahap,kekeh,cewek,dadar,kamis,yuris,kisam,manga,gardu,lupus,tiner,pandu,feses,audit,tahir,pacar,mepet,gegas,abrar,depot,jamak,kasir,antik,kenya,intel,strip,saham,silat,nalar,zahid,rapel,count,unduh,lezat,arang,sesat,lawar,ambar,ahsan,bawah,etnik,ember,bilah,kurun,kesan,mayam,sereh,ipung,tekun,serut,sebab,kerek,finis,nekat,ander,bajul,gokil ================================================ FILE: .scripts/update.js ================================================ const path = require("path"); const fs = require("fs/promises"); const { Octokit } = require("@octokit/rest"); const answerPath = path.join(__dirname, "answers.csv"); const wordsPath = path.join(__dirname, "whitelist.csv"); // sync with update.yml const TIMEZONE_OFFSET = 9; const readCsv = (filePath) => fs.readFile(filePath, "utf-8").then((text) => text .split(",") .map((s) => s.trim()) .filter(Boolean) ); async function insertWord(answers, answer) { if (process.env.GITHUB_ACTIONS) { // GH actions await writeCommit(answers.concat(answer).join(",") + "\n"); } else { // dev testing await fs.writeFile(answerPath, answers.concat(answer).join(","), "utf-8"); } } async function writeCommit(data) { const token = process.env.GITHUB_TOKEN; if (!token) { throw new Error("Missing GitHub token env in `GITHUB_TOKEN`"); } const FileInfo = { owner: "pveyes", repo: "katla", path: ".scripts/answers.csv", sha: "main", }; const octokit = new Octokit({ baseUrl: "https://api.github.com", auth: `token ${token}`, }); const response = await octokit.repos.getContent(FileInfo); const { sha } = response.data; octokit.repos.createOrUpdateFileContents({ ...FileInfo, message: "Insert new answer", content: Buffer.from(data).toString("base64"), sha, branch: "main", }); } function getMidnightDate() { const now = new Date(); now.setHours(now.getHours() + TIMEZONE_OFFSET); const deltaToMidnightMinutes = 60 - now.getMinutes(); now.setMinutes(now.getMinutes() + deltaToMidnightMinutes); return now; } async function main() { const [usedWords, allWords] = await Promise.all([ readCsv(answerPath), readCsv(wordsPath), ]); const validWords = allWords.filter((word) => !usedWords.includes(word)); // use let to allow secret words let word = validWords[Math.floor(Math.random() * validWords.length)]; const secretDate = process.env.SECRET_DATE; const secretWord = process.env.SECRET_WORD; if (secretDate && secretWord) { const date = getMidnightDate(); const [mm, dd] = secretDate.split("-").map(Number); if (date.getDate() == dd && date.getMonth() + 1 === mm) { word = secretWord; } } await insertWord(usedWords, word); console.log("New word inserted", word); } main().catch((error) => { console.error("Failed", error); process.exit(1); }); ================================================ FILE: .scripts/whitelist.csv ================================================ abadi,abang,abate,abawi,abaya,abbas,abjad,abrar,absah,absen,abung,abuya,acang,acara,acian,acuan,adang,adika,admin,adnan,adobe,adong,aduan,adven,advis,afair,afdal,afiks,agama,agami,agogo,agung,ahang,ahsan,ajaib,ajang,ajoli,akbar,akhir,akmal,akrab,aksen,akses,akson,aktif,aktor,alaan,alami,alang,alarm,album,alias,alibi,alien,allah,almon,alpin,altar,amang,ambah,ambai,ambar,amber,ambil,ambin,amboi,ambon,amina,amino,amorf,ampai,ampar,ampas,ampat,ampel,ampuh,ampun,amsal,anang,anani,ancai,ancam,ancol,andai,andal,andan,andap,andar,ander,andil,andir,aneka,angan,angel,angga,angin,angit,angka,angke,angsa,anime,animo,anjar,ansar,antar,antek,antik,anton,antre,anyam,anyar,anyir,aorta,apaan,apnea,apoge,april,apron,apung,arang,areal,arena,argon,ariel,aries,aroma,arsip,artis,arung,arwah,asasi,asesi,asian,asing,askar,aspek,aswad,asyik,atase,atlas,atlet,atung,audio,audit,aulia,aurat,autis,avgas,avtur,awang,aytek,baang,babad,babak,baban,babar,babat,bacan,bacin,bacok,badai,badak,badal,badan,badar,badau,badik,badui,badur,badut,bagai,bagan,bagas,bagus,bahan,bahar,bahas,bahau,bahri,bahwa,baiat,bajaj,bajak,bajik,bajir,bajul,bakal,bakam,bakar,bakat,bakau,bakda,bakmi,bakso,bakti,bakul,balad,balah,balai,balak,balap,balas,balen,balet,balig,balik,balok,bambu,banda,bando,banga,banta,bantu,banua,banyo,banyu,bapak,barai,barak,baran,baras,barat,barel,baret,barik,baris,barok,baros,basah,basar,basin,basir,basis,basmi,batak,batal,batas,batik,batin,batok,batuk,batur,baung,bawah,bayak,bayam,bayar,bayur,bazar,bebal,beban,bebas,bebek,beber,bebop,becak,becek,becik,becus,bedah,bedak,bedil,beduk,begah,begal,beguk,behel,bejat,bekal,bekam,bekas,bekel,beken,belah,belas,belia,belit,belok,belom,beluk,belum,belut,benak,benar,benci,benda,benih,benua,benur,berak,beras,berat,berau,beres,beruk,besan,besar,beser,besit,besok,besuk,besut,betah,betet,betis,beton,betul,bewok,bezit,biang,biara,biasa,biaya,bibel,bibir,bibit,bidah,bidai,bidak,bidan,bidar,bidik,bigot,bihun,bijak,bijih,bikin,biksu,bilah,bilal,bilas,bilik,binal,binar,biner,binti,biola,bioma,biota,birai,bisai,bisik,bison,bisul,bivak,blang,bleng,blitz,blong,blues,bobin,bobol,bobot,bocah,bocor,bodoh,bogem,bogor,bokap,bokeh,bokor,boleh,bolos,bonar,bongo,bonus,borok,boros,bosan,botak,botia,botol,brata,bruto,buana,buang,buani,buaya,bubar,bubuk,bubur,budak,buduk,bufet,bugar,bugis,bujet,bujuk,bujur,bukan,buket,bukit,bukti,bulai,bulak,bulan,bulat,bulir,buluh,bulus,bumbu,bunda,bunga,buntu,bunuh,bunut,bunyi,buram,burik,buron,bursa,buruh,buruk,busai,busik,busuk,busur,butik,butir,buton,butuh,butut,buyar,buyut,cabai,cabul,cabut,cacah,cacak,cacar,cacat,cadai,cadar,cadas,cadel,cagar,caima,caing,cakap,cakar,cakep,cakra,cakup,calon,camat,canai,canda,candi,candu,capai,capek,caren,carik,carok,carut,catat,catur,cawan,ceban,cebok,cebol,cebur,cecak,cecer,cedok,ceeng,cegah,cegat,ceguk,cekah,cekal,ceker,cekik,cekok,cekup,cekut,celah,celik,celuk,celup,celus,cemar,cemas,cempe,cengi,cengo,cepat,cepek,ceper,cepol,cepuk,cerah,cerai,cerek,ceria,cerih,cerna,ceruk,cerun,cetai,cetak,ceter,cetus,cewek,china,cibuk,cicil,cicit,ciduk,cihui,cikal,cikar,cilik,cilok,cimol,cinde,cinta,cipta,ciput,citra,clear,cling,coban,cocok,cogan,coket,colek,colok,comek,comel,copet,copot,corak,coret,count,cowok,crash,cuaca,cubit,cucun,cucup,cukai,cukup,cukur,culas,culun,cuman,curah,curai,curam,curat,dabal,dabol,dadap,dadar,daeng,dahak,dahan,daing,dairi,dakar,dakon,daksa,dakwa,dalam,dalem,dalih,dalil,damai,damar,danau,danda,dandi,dange,dansa,danur,danus,dapat,dapur,darah,darat,darma,darun,dasar,dassi,datar,datau,datin,datuk,datum,dawai,dawan,dawet,dayah,dayak,debak,debat,debit,debum,debus,debut,decak,dedek,degan,degen,deguk,dekan,dekap,dekat,dekil,dekor,delas,delik,delta,demak,demam,demen,denah,denda,dendi,denim,denok,depak,depan,depok,depot,derai,deran,derap,deras,derbi,derek,derit,derma,derus,desak,desau,desil,desis,detak,detap,detik,deuce,dewan,diare,diari,didih,didik,digit,dikit,diksi,dikte,diler,dinar,dinas,dinda,dipol,disel,diska,disko,diuji,doang,dobel,dodot,dogma,dolar,dolok,domba,dompu,donat,donor,dorbi,dorna,dosen,dosis,dower,doyan,drama,duafa,dubur,duduk,duhai,dukuh,dukun,dulur,dumai,dumbo,dunia,dupak,dusta,dusun,duvet,duwet,eboni,eceng,edisi,egois,ejaan,eksak,ekses,eksil,eksis,elang,elips,elite,elung,elusi,email,emang,emban,embat,ember,embun,emisi,emoji,emosi,empal,empat,empot,empuk,encer,encik,encim,encok,encot,endus,eneng,engku,entah,entit,entok,entri,envoi,enzim,eropa,erosi,esais,ester,etana,etape,etika,etnik,etnis,eweai,eyang,faali,fadil,fajar,fakir,faksi,fakta,falah,falaj,fardu,fasad,fasia,fasih,fasis,fatah,fatal,fatir,fatwa,fault,fauna,felon,feral,feses,fiber,figur,fikih,fiksi,filum,final,finis,firma,fisik,fitri,fitur,fjord,flora,fluks,fobia,fokus,folat,folio,fondu,forte,forum,fosil,foton,foyer,frasa,freon,front,fulan,fulus,fungi,fusia,fyord,gabah,gabus,gacor,gadai,gadis,gaduh,gafur,gagah,gagak,gagal,gagap,gagas,gahar,gajah,gajih,galah,galak,galat,galau,galih,galon,galuh,galur,gamat,gamet,gamis,ganar,ganas,ganda,ganja,ganti,gapai,gaple,garam,garap,garda,gardu,garis,garpu,garut,gatal,gatot,gatra,gaung,gawai,gawan,gawar,gawat,gawir,gayuh,geber,gebos,gebuk,gedor,gegap,gegas,geger,gelak,gelap,gelar,gelas,gelut,gemar,gempa,gemuk,genap,genom,genre,genta,genuk,genus,gerah,gerai,gerak,geram,gerha,geser,gesit,getah,getar,getas,getek,getok,getol,gigih,gigit,gimik,gincu,gipsi,gitar,globe,glosa,gobak,godok,gojek,gokar,gokil,golok,gores,gorok,gorup,gosip,gotik,gowes,gowok,goyah,graha,grasi,gravi,griya,guano,gubuk,gudeg,gugat,gugup,gugur,gugus,gulai,gulam,gulat,gulir,gulma,gumam,gunci,gurah,gurau,gurih,gurit,guruh,gurun,gurur,gusar,gusti,gusur,guyon,guyub,guyur,habib,habil,habis,hadap,hadas,hadir,hadis,hafal,hafiz,hajar,hajat,hakim,halal,halau,halim,halte,halus,halwa,hamba,hamil,hampa,hanif,hantu,hanya,hapus,haram,harap,harga,haris,harta,harum,harun,harus,hasan,hasil,hasta,hasut,hatta,hawar,hayat,hebat,heboh,heiho,helai,hemat,hendi,henry,henti,heran,hewan,hibah,hibur,hidro,hidup,hiena,hijab,hijau,hilal,hilir,himne,hindu,hiruk,hirup,hisab,hitam,hoaks,honje,honor,horas,horor,hotel,hujan,hujat,hukum,human,humor,humus,huruf,hutan,ialah,iblis,ideal,idiom,idiot,idola,idris,iftar,ihram,ihsan,ihwal,iklan,iklim,ikram,ikrar,ilafi,ilahi,ilham,ilusi,ilyas,imago,imaji,imani,imbal,imbas,imbau,imbuh,imlek,impor,impun,imsak,inang,indah,india,indie,indik,indra,induk,infak,infus,ingat,ingin,ingus,injak,injil,input,insaf,insan,inses,intan,intel,intim,intip,intro,ipung,irama,irian,iring,ironi,isbat,iseng,ishak,isian,islah,islam,islan,istri,iuran,ivori,jabal,jabat,jabir,jagat,jagur,jahat,jahil,jahit,jajah,jajak,jajal,jajan,jajar,jaket,jaksa,jakun,jalak,jalal,jalan,jalar,jalen,jalil,jalin,jalur,jamak,jamal,jambe,jambi,jambu,jamil,jamin,jamur,janah,janda,janin,janji,janur,japan,jarak,jarum,jasad,jatah,jatuh,jauza,jawab,jawat,jawil,jebak,jebol,jegal,jegil,jegog,jejak,jelas,jelek,jeluk,jemah,jemur,jenis,jenuh,jepit,jeram,jerat,jerih,jerit,jersi,jeruk,jetis,jewer,jibun,jidur,jihad,jijik,jilid,jimak,jimat,jimbe,jinak,jingu,jiran,jodoh,joget,joglo,johan,johar,joker,jomlo,jompo,jorok,jotos,jreng,juang,juara,jubah,jubin,judes,judul,juita,jujur,jujut,julur,jumat,jumbo,jumpa,junta,junub,jurai,jurik,jurus,kabar,kabau,kabel,kabil,kabin,kabir,kabul,kabur,kabut,kacau,kacer,kacuk,kadal,kadar,kader,kadir,kafir,kaget,kagum,kahar,kaili,kakak,kakao,kakap,kakek,kakus,kalah,kalak,kalam,kalap,kalau,kalbu,kaldu,kalem,kalih,kalis,kalke,kalor,kamal,kamar,kamba,kambu,kameo,kamil,kamis,kamus,kanal,kanan,kanda,kania,kanji,kanya,kapak,kapal,kapan,kapas,kapat,kapok,kapuk,kapur,karah,karam,karat,karel,karet,kargo,karib,karim,karma,karst,kartu,karya,kasap,kasar,kasep,kaset,kasia,kasih,kasim,kasir,kasta,kasui,kasur,kasus,katai,katak,katib,katik,katun,kawah,kawak,kawal,kawan,kawat,kawin,kawuk,kayak,kayuh,kebab,kebal,kebun,kebut,kecak,kecam,kecap,kecil,kecoh,kedah,kedai,kedam,kedap,keder,kedok,kedua,kefir,kejam,kejer,kejut,kekal,kekar,kekat,kekau,kekeh,kelab,kelak,kelam,kelar,kelas,kelih,kelik,kelok,kelor,kelua,keluh,kelus,kemah,kemal,kemam,kemas,kemih,kemis,kemit,kemon,kenal,kenan,kenap,kenek,kenop,kento,kenya,kenyi,keong,kepak,kepal,kepuh,kepul,kerah,kerak,keram,keran,kerap,keras,kerau,kerek,keren,keris,kerja,kerok,keruh,keruk,kesah,kesal,kesan,kesek,kesel,keset,ketan,ketat,ketik,ketok,ketua,ketuk,ketut,khair,khaki,khiar,kiara,kibor,kicau,kidal,kidul,kikil,kikis,kikuk,kilah,kilas,kilat,kilau,kimia,kinan,kipas,kiper,kirab,kiras,kirim,kisah,kisam,kista,kitab,kitir,klaim,klien,klise,kluet,koala,kobra,kocak,kocek,kocok,kodak,kodok,kokoh,kokok,kolak,kolam,kolom,kolot,komen,komet,komik,kompi,konco,konde,konga,konon,konte,konus,koper,koplo,kopra,koran,korea,korek,korps,korve,kosen,kotak,kotor,kover,koyak,krans,kreol,kribo,krida,kring,kriya,kroco,kroma,kromo,kuaci,kuala,kuang,kuasa,kubah,kubik,kubis,kubur,kubus,kucek,kucir,kucup,kucur,kudus,kueri,kukis,kukuh,kukus,kulit,kulon,kulup,kumai,kuman,kumat,kumis,kumuh,kumur,kunci,kunta,kunut,kuota,kupas,kupat,kupon,kurau,kurdi,kuria,kurir,kurma,kursi,kurun,kurus,kurva,kusam,kusir,kusta,kutai,kutip,kutub,kuyup,label,labil,labuh,lafal,lafaz,lahab,lahad,lahan,lahap,lahar,lahat,lahir,lajur,lakon,laksa,lalai,lalap,lalat,lalau,lamak,laman,lamar,lampu,lamun,langi,langu,lapak,lapar,lapis,lapor,lapuk,laras,laris,larut,larva,laser,lasik,lasut,latah,latar,laten,latif,latih,latin,lawah,lawak,lawan,lawar,lawas,layak,layar,layer,lazim,lebah,lebak,lebam,lebar,lebat,lebih,lebur,lecet,ledak,ledek,ledes,legal,legam,legit,legok,leher,lekas,lekat,lekir,lekuk,lelah,lelap,leleh,lelet,lemah,lemak,leman,lemas,lembu,lemon,lensa,lepak,lepas,lepuh,lerai,lerak,letak,letih,level,lever,lewat,lezat,liang,libas,libra,libur,licik,licin,lidah,ligat,lihai,lihat,lilin,lilit,limas,limau,limfa,limit,linda,linen,lines,liong,lipai,lipat,lipid,lirih,lirik,liris,lisan,liter,liwan,liwet,lobak,lodeh,logam,logat,logis,lokal,loker,loket,lokus,lolak,lolos,lomba,longo,lorek,lotak,lotre,lotus,loyal,luang,luber,lubuk,ludah,ludes,lugas,luhur,lukas,lukis,luluh,luluk,lulus,lulut,lumer,lumut,lunak,lunar,lunas,lupus,luput,lurah,lurik,luruh,lurus,lusin,lusuh,lutut,luwes,mabuk,macam,macan,macet,madam,madya,mafia,magis,magma,mahal,mahar,mahdi,mahir,majas,makam,makan,makar,makin,makna,makro,maksi,malah,malam,malas,malik,mamah,mamak,maman,mamat,mambo,mambu,mampu,manah,manda,mandi,mandu,manga,mania,manik,manis,manja,manna,manta,mantu,manuk,mapan,mapor,marah,marak,maras,maret,marga,maria,maros,marut,masak,masif,masih,mason,massa,masuk,matan,matik,matra,maung,mawar,mawas,mayam,mayat,mayor,mayur,mebel,medan,media,medik,medio,medis,megah,megan,mekar,melek,melon,memar,menit,menor,menua,menur,meong,mepet,merah,merak,mercu,merdu,merek,merta,mesem,meses,mesin,mesiu,meski,mesra,mesti,mesum,metal,meter,metro,mewah,mezzo,mikro,milad,milik,milir,mimpi,minat,minda,minim,minor,minta,minum,minus,mirah,mirip,miris,misal,misil,mitos,mitra,mizan,mobil,modal,model,modem,modis,modul,modus,mogok,mohon,molek,molen,molor,momen,momok,moral,moron,morse,motel,motif,motor,muara,mudah,mudik,mufti,mujur,mukim,mulai,mulas,mulia,muluk,mulus,mulut,murad,murah,murai,mural,muram,murid,murka,murni,musik,musim,musti,musuh,nabil,nadim,nadir,nafsu,nahak,nahas,najis,nakal,nalar,nambi,namun,nanah,nanas,nanti,napas,nasab,nasal,nasib,natal,natar,nazar,nazir,ndoro,nduga,necis,nekat,nenek,netra,ngada,ngawi,ngeri,ngilu,ngoyo,ngung,niaga,nihil,nikah,nikel,nilai,nilam,nilon,nimfa,ninik,ninja,nipah,nipis,nisab,nisan,nista,nomor,norma,notes,novel,novum,nuget,nujum,nusuk,nuzul,nyala,nyali,nyata,nyawa,nyepi,nyeri,nyiur,oasis,obeng,objek,oblak,obral,obras,oknum,oktaf,oktet,oleng,oliva,ombak,omega,omong,ompok,omzet,oncom,onsen,ontel,opera,opini,opsen,optik,orang,orasi,orbit,order,organ,orion,osean,ovasi,oyong,pacar,pacet,pacul,padak,padam,padan,padar,padas,padat,padma,pagar,pagon,paham,pahat,pahit,paing,pajak,pakai,pakal,pakan,pakar,pakem,paket,pakis,paksa,paksi,pakta,palak,palar,palas,palem,palet,palit,palka,palsu,pamah,paman,pamer,pamit,panah,panai,panas,panci,panda,pandu,panel,panen,panik,panji,panti,papah,papan,papar,papua,paraf,parah,parak,paras,parau,paris,parit,parsi,paruh,parut,pasai,pasak,pasal,pasan,pasar,paser,pasif,pasir,pasok,pasta,pasti,patah,patar,paten,patih,patil,patin,patok,patri,patuh,patuk,patut,pause,pawai,payah,payau,payet,peang,pecah,pecat,pecel,pecuk,pedal,pedas,pedih,pedis,pegal,pegan,pegon,pejal,pejam,pekan,pekat,pekik,pelak,pelan,pelat,pelek,pelet,pelik,pelit,pelor,penad,penat,penda,penuh,penyu,pepah,pepes,perah,perak,peran,peras,perca,pergi,perih,perlu,peron,perut,pesan,pesat,pesek,pesta,pesut,petak,petes,petik,petir,petis,petra,petuk,piala,piano,piatu,pidie,pihak,pijar,pijat,pikap,pikat,piket,pikir,pikul,pikun,pilah,pilar,pilek,pilih,pilot,pilus,pines,pinta,pintu,pinus,pipih,pipil,pipis,pipit,pisah,pisau,pitak,pitam,piton,pivot,plang,plano,plato,plaza,pleno,plong,pluto,pohon,poise,pojok,poker,poket,pokok,polan,polar,polio,polip,polis,polos,pompa,ponco,popok,porno,poros,porsi,porto,porus,prabu,praja,premi,prima,promo,prosa,puasa,pucat,pucuk,pudar,pudel,puguh,puing,puisi,pujut,pukul,pulas,pulau,pulen,pulih,pulsa,puluh,pulut,punah,punai,punca,pundi,punia,punti,punuk,punya,pupuk,pupus,puput,purba,purut,purwa,pusar,pusat,puspa,putar,putih,putra,putri,putus,putut,puyer,puyuh,qanun,rabat,rabun,racik,racun,radar,raden,radif,radin,radio,rafia,ragam,rahib,rahim,rajab,rajin,rajut,raket,rakit,rakus,rakyu,ralat,ramah,ramai,rambu,ramen,ranah,ranau,rancu,randi,randu,rangu,ranji,ranti,rapak,rapat,rapel,rapor,rapuh,raras,rasau,rasio,rasis,rasul,ratna,ratus,raung,rawan,rawas,rawat,rawit,rawon,rayap,rayon,razia,rebab,rebah,rebus,rebut,receh,redah,redam,redup,regan,regas,rehab,rehal,rehat,rejan,rekam,rekan,rekap,rekor,reksa,remeh,remuk,repot,resah,resan,resek,resep,reses,reset,resik,resin,resmi,resor,resto,restu,retak,retas,retro,reuni,rewel,reyot,rezim,riang,ribet,ribut,ricuh,rijal,rilis,rimba,rindu,risau,riset,ritel,ritme,ritus,rival,robek,roboh,robot,rogoh,roker,roket,rokok,roman,rompi,ronda,ronde,rondo,rotan,rouge,rouks,royal,royan,ruang,rubah,rubik,rubin,rudal,rugbi,ruing,rujak,rujuk,rukun,rumah,rumit,rumor,rumpi,rumus,rungu,runut,rural,rusak,rusuk,rutin,ruwah,ruwet,sabah,saban,sabar,sabda,sabet,sabit,sabtu,sabuk,sabun,sabut,sadap,sadar,sadis,safar,saham,sahih,sahur,sahut,saing,sains,sajak,sakar,sakat,sakau,sakit,saksi,sakti,salaf,salah,salak,salam,salat,saldo,saleh,salem,salep,salib,salim,salin,salip,salju,salon,salto,salur,salut,salvo,samad,saman,samar,samas,samba,sambi,samin,sampo,samsu,sanad,sanak,sanca,sanda,sandi,sanga,santa,santo,sapun,saraf,saran,sarat,saren,sarmi,sasis,satai,satin,satwa,saung,sauri,sause,savan,sawah,sawai,sawer,sawit,sayap,sayat,sayid,sayup,sayur,sebab,sebal,sebar,sebel,sebum,sebut,sedan,sedap,sedia,sedih,sedot,seduh,seeng,segan,segar,segel,sehat,seisi,sejak,sejuk,sekak,sekam,sekar,sekat,seken,sekip,sekon,seksi,sekte,selai,selak,selam,selar,selat,selip,selir,selok,seluk,semai,semak,semen,semok,semua,semur,semut,senam,senar,senat,sendi,sendu,senil,senin,senja,sepah,sepak,sepat,sepen,sepuh,serah,serai,seram,serap,serat,serba,serbu,sereh,serep,seret,serge,serit,serta,seruk,serut,serve,sesak,sesar,sesat,setan,setek,setel,setia,setir,setop,setor,setra,setup,sewot,siaga,siang,siapa,siber,sibuk,sidat,sidik,sifat,sigap,siger,sigma,sihir,sikap,sikat,sikka,siksa,silam,silat,silau,silet,silih,simak,sinar,singa,sinis,sinka,sinom,sinus,sipil,sipir,sipit,siput,sirah,siram,sirih,sirip,sirna,sirop,sisik,sisil,sisir,siswa,siswi,situs,skala,skema,skors,skrin,skrip,skrub,skuad,slang,sling,sobat,sobek,sodok,sodor,sogan,sohar,sohib,soket,solar,solid,solok,sonar,sonde,sopan,sopir,sorak,sorot,sosok,sosor,sowan,spasi,spion,spons,spora,sport,start,stasi,stela,steno,stepa,stres,strip,strok,studi,suaka,suami,suang,suara,suari,suatu,subak,subam,subuh,subur,sudah,sudan,sudut,sugar,sugih,suite,sujud,sukar,sukma,sukuk,sukun,sulam,sulap,sulbi,sulit,sumah,sumba,sumbu,sumur,sunah,sunan,sunat,sunda,sunyi,super,supit,surah,suram,surat,surau,surga,suruh,surut,surya,susah,susuk,susul,susun,susut,sutan,sutra,suwir,swike,syair,syekh,syiar,syura,taala,tabah,tabat,tabel,tabib,tabik,tabir,tabuh,tabur,tadah,tafia,tagih,tahan,tahap,tahar,tahir,tahun,tajak,tajam,tajin,tajir,tajuk,takar,taksi,taksu,takut,takwa,talak,talam,talas,talun,talut,tamam,taman,tamat,tambi,tambo,tamil,tampo,tanah,tanak,tanam,tanda,tandu,tango,tania,tanpa,tante,tanur,tanya,taoco,taoge,tapai,tapak,tapal,tapin,tapis,taraf,tarif,tarik,tarot,taruh,tasik,tatah,tatak,tatal,tatap,tawaf,tawar,tawon,tawur,tayub,tebak,tebal,tebar,tebas,tebet,tebus,teduh,tegak,tegal,tegar,tegas,tegel,teguh,teguk,tegur,tekad,tekan,tekat,tekel,teken,tekor,tekuk,tekun,telah,telak,telan,telap,telat,telor,teluk,telur,teman,tempa,tempe,tempo,tenar,tenda,tenis,tenor,tensi,tentu,tenun,teori,tepat,tepis,tepok,tepuk,tepus,teras,terik,terka,teror,terpa,terus,tesis,tesla,tetap,tetas,teteh,tetes,tetra,tetua,teuku,tewas,tiada,tiang,tiara,tiban,tidak,tidur,tifus,tikar,tiket,tikus,tilas,timah,timpa,timun,timur,tiner,tinja,tinju,tinta,tipar,tipis,tirai,tiram,tiran,tiris,tirta,tirus,titan,titel,titen,titik,titip,titis,tiwul,tobat,togok,tokek,token,tokoh,tolak,tolan,tolok,toman,tomas,tomat,tonga,topan,topik,torsi,total,totok,totol,towel,trafo,trans,trofi,tuang,tuban,tubin,tubir,tubuh,tuduh,tugas,tuhan,tujuh,tukar,tukas,tukik,tukul,tular,tulat,tulen,tulip,tulis,tulus,tumis,tumit,tumor,tunai,tunas,tunda,tunik,tupai,turap,turis,turki,turun,turut,tusuk,tutor,tutul,tutup,tutur,tutut,tuyul,uapan,ubung,udang,udara,udeng,ujang,ujian,ujung,ukhti,ulama,ulang,ulkus,ulser,ultah,ulung,umami,umara,umbar,umbul,umpan,umpet,umrah,undak,unduh,undur,union,unjuk,unsur,untai,untuk,untun,unyil,upaya,upeti,urban,urgen,urine,urung,usaha,usang,using,uskup,ustaz,usung,utama,utang,utara,uwete,vadem,vakum,valas,valid,vasal,vegan,venus,versi,vespa,veste,video,vinil,viola,viral,virga,virgo,virus,vista,visum,vital,vodka,vokal,volan,vonis,wabah,wadah,wadak,waduh,waduk,wafat,wafer,wagyu,wahai,wahid,wahyu,wajah,wajan,wajar,wajib,wajik,wakaf,wakil,waktu,walau,walet,wanam,wanda,wangi,waras,warga,waria,waris,warna,warta,wasit,watak,water,welas,wesel,wetan,weton,wilis,windu,wisma,witir,wiwik,wujud,wukuf,wulan,wushu,xenia,yacht,yahya,yaitu,yakin,yakni,yakub,yasti,yatim,yesus,yogia,yolks,yonif,yonko,yudas,yunda,yunus,yuris,yusuf,zabur,zahid,zakat,zalim,zaman,zapin,zebra,zikir,zirah,zombi,zuhud,zuhur,zumba ================================================ FILE: .vscode/settings.json ================================================ { "prettier.singleQuote": false, "prettier.arrowParens": "always" } ================================================ FILE: README.md ================================================ # Katla Permainan kata harian. ~~Imitasi~~ Terinspirasi dari [Wordle](https://www.powerlanguage.co.uk/wordle/) [![Powered by Vercel](https://www.datocms-assets.com/31049/1618983297-powered-by-vercel.svg)](https://vercel.com?utm_source=katla&utm_campaign=oss) ================================================ FILE: components/Alert.tsx ================================================ import toast, { Toaster } from "react-hot-toast"; export default function Alert() { return ( ); } Alert.options = { duration: 750, className: "bg-gray-900 text-white dark:bg-white dark:text-black text-center font-semibold py-2 px-3 rounded-sm", }; interface AlertOptions { duration?: number; cb?: () => void; id: string; } Alert.show = (message: string, options: AlertOptions) => { const duration = options.duration || Alert.options.duration; let formatted: any = message; if (message.includes("\n")) { const lines = message.split("\n"); formatted = (
{lines.map((line, i) => { if (i === lines.length) { return line; } return ( <> {line}
); })}
); } toast(formatted, { id: options.id, duration, }); if (options.cb) { setTimeout(() => { options.cb(); }, duration); } }; ================================================ FILE: components/App.tsx ================================================ import { useEffect, useRef, useState } from "react"; import Board from "./Board"; import Keyboard from "./Keyboard"; import { GameStats } from "../utils/types"; import { decode } from "../utils/codec"; import { handleGameComplete, getFailureMessage } from "../utils/message"; import { checkHardModeAnswer, getAnswerStates, verifyStreak, } from "../utils/game"; import { Game } from "../utils/types"; import { trackEvent } from "../utils/tracking"; import Alert from "./Alert"; interface Props { game: Game; stats: GameStats; words: string[]; setStats: (stats: GameStats) => void; showStats: () => void; // optional event handlers for different game modes onSubmit?: (game: Game, userAnswer: string) => void; onComplete?: typeof handleGameComplete; } export default function App(props: Props) { const { game, stats, setStats, showStats, words } = props; const [invalidAnswer, setInvalidAnswer] = useState(false); const isAnimating = useRef(null); const answer = decode(game.hash); function handlePressChar(char: string) { // ignore if already finished if (game.state.answers[game.state.attempt - 1] === answer) { return; } if (isAnimating.current) { return; } if (!game.state.enableFreeEdit && char === "_") { return; } game.setState({ ...game.state, answers: game.state.answers.map((answer, i) => { if (i === game.state.attempt) { if (answer.length === 5) { if (answer.includes("_")) { return answer.replace("_", char.toLowerCase()); } // do nothing return answer; } return answer + char.toLowerCase(); } return answer; }), }); } function handleBackspace() { if (isAnimating.current) { return; } game.setState({ ...game.state, answers: game.state.answers.map((answer, i) => { if (i === game.state.attempt) { return answer.slice(0, -1); } return answer; }), }); } function handleSubmit() { if (isAnimating.current) { return; } // ignore submission user already know the answer if ( // already fail game.state.attempt === 6 || // already found the answer game.state.answers[game.state.attempt - 1] === answer ) { return; } const userAnswer = game.state.answers[game.state.attempt] .split("") .filter((char) => char !== "_") .join(""); if (userAnswer.length < 5) { markInvalid(); Alert.show("Tidak cukup huruf", { id: "answer" }); return; } if (!words.includes(userAnswer.toLowerCase())) { markInvalid(); Alert.show("Tidak ada dalam KBBI", { id: "answer" }); game.trackInvalidWord(userAnswer); return; } if (game.state.enableHardMode && game.state.attempt > 0) { const [isInvalid, unusedChar, leterIndex] = checkHardModeAnswer( game.state, answer ); if (isInvalid) { markInvalid(); if (leterIndex) { Alert.show(`Huruf ke-${leterIndex} harus ${unusedChar}`, { id: "answer", }); } else { Alert.show(`Huruf ${unusedChar} harus dipakai`, { id: "answer" }); } return; } } props.onSubmit?.(game, userAnswer); setInvalidAnswer(false); game.submitAnswer?.(userAnswer, game.state.attempt + 1); game.setState({ ...game.state, answers: game.state.answers.map((answer, i) => { if (i === game.state.attempt) { return userAnswer; } return answer; }), attempt: game.state.attempt + 1, lastCompletedDate: game.state.lastCompletedDate, }); isAnimating.current = true; setTimeout(() => { isAnimating.current = false; if (answer === userAnswer) { if (typeof game.submitAnswer === "function") { game.setState({ ...game.state, answers: game.state.answers.map((answer, i) => { if (i === game.state.attempt) { return userAnswer; } return answer; }), attempt: game.state.attempt + 1, lastCompletedDate: new Date().getTime(), }); return; } props.onComplete?.({ hash: game.hash, attempt: game.state.attempt + 1, stats: stats, cb: showStats, }); trackEvent("succeed", { hash: game.hash, attempt: game.state.attempt + 1, }); const currentStreak = stats.currentStreak + 1; game.setState({ ...game.state, answers: game.state.answers.map((answer, i) => { if (i === game.state.attempt) { return userAnswer; } return answer; }), attempt: game.state.attempt + 1, lastCompletedDate: new Date().getTime(), }); setStats({ distribution: { ...stats.distribution, [game.state.attempt + 1]: stats.distribution[game.state.attempt + 1] + 1, }, currentStreak, maxStreak: Math.max(stats.maxStreak, currentStreak), }); } else if (game.state.attempt === 5) { if (typeof game.submitAnswer === "function") { game.setState({ ...game.state, answers: game.state.answers.map((answer, i) => { if (i === game.state.attempt) { return userAnswer; } return answer; }), attempt: game.state.attempt + 1, lastCompletedDate: new Date().getTime(), }); return; } trackEvent("failed", { hash: game.hash }); setStats({ distribution: { ...stats.distribution, fail: stats.distribution.fail + 1, }, currentStreak: 0, maxStreak: stats.maxStreak, }); const failureMessage = getFailureMessage( stats, getAnswerStates(game.state.answers[game.state.attempt], answer) ); Alert.show(`${failureMessage}. Jawaban: ${answer}`, { id: "finish", duration: 1250, cb: showStats, }); } }, 400 * 6); } function markInvalid() { setInvalidAnswer(true); setTimeout(() => { setInvalidAnswer(false); }, 600); } // auto resize board game to fit screen useEffect(() => { if (!game.ready) { return; } function handleResize() { const katla = document.querySelector("#katla") as HTMLDivElement; const footerHeight = document.querySelector("footer")?.getBoundingClientRect()?.height ?? 0; const maxTileHeight = window.innerHeight - document.querySelector("#header").getBoundingClientRect().height - document.querySelector("#keyboard").getBoundingClientRect().height - footerHeight; document.querySelector("#game-bar")?.getBoundingClientRect()?.height ?? 0; const maxTileSize = Math.min(maxTileHeight, window.innerWidth); const singleTileSize = Math.max(Math.floor((maxTileSize - 30) / 6), 62); const tileWidth = 5 * singleTileSize + 42; katla.style.height = maxTileSize + "px"; katla.style.width = tileWidth + "px"; } handleResize(); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [game.ready]); return ( <>
); } ================================================ FILE: components/Board.tsx ================================================ import Tile from "./Tile"; import { Game, GameState } from "../utils/types"; import { decode } from "../utils/codec"; import { getAnswerStates } from "../utils/game"; import { FLIP_ANIMATION_DELAY_MS } from "../utils/constants"; interface Props { game: Game; invalidAnswer: boolean; } export default function Board(props: Props) { const { game, invalidAnswer } = props; const answer = decode(game.hash); function handlePress(row: number, index: number) { if (game.state.enableFreeEdit && row === game.state.attempt) { game.setState({ ...game.state, answers: game.state.answers.map((answer, i) => { if (i === game.state.attempt) { return answer.slice(0, index) + "_" + answer.slice(index + 1); } return answer; }), }); } } return (
{Array(6) .fill("") .map((_, i) => { let userAnswer = game.state.answers[i] ?? ""; userAnswer += " ".repeat(5 - userAnswer.length); const answerStates = getAnswerStates(userAnswer, answer); const lieBoxes = game.state.lieBoxes?.[i]; const isTheAnswer = answerStates.every((answer) => answer === "c"); return (
{userAnswer.split("").map((char, index) => { let state = null; if (i < game.state.attempt) { state = answerStates[index]; const [forcedColumn, forcedState] = lieBoxes ?? []; if ( // only replace the box if liar mode is enabled game.state.enableLiarMode && // and the column matches forcedColumn === index && // and it's not the answer !isTheAnswer ) { state = forcedState; } } const isInvalid = invalidAnswer && i === game.state.attempt; return ( handlePress(i, index)} /> ); })}
); })}
); } ================================================ FILE: components/Container.tsx ================================================ import { PropsWithChildren } from "react"; export default function Container(props: PropsWithChildren<{}>) { return (
{props.children}
); } ================================================ FILE: components/EmojiRain.tsx ================================================ import { ComponentRef, useEffect, useRef, useState } from "react"; const CUSTOM_EVENT_NAME = "emoji-rain"; export default function EmojiRain() { const canvasRef = useRef>(null); const [emoji, setEmoji] = useState(null); const timeoutRef = useRef(); useEffect(() => { if (!emoji) { return; } if (canvasRef.current === null) { return; } const w = window.innerWidth; const h = window.innerHeight; const ctx = canvasRef.current.getContext("2d"); const bodyBackground = window.getComputedStyle( document.body ).backgroundColor; canvasRef.current.width = w; canvasRef.current.height = h; ctx.fillStyle = bodyBackground; ctx.fillRect(0, 0, w - 1, h - 1); const numOfEmojis = randomBetween(20, 30); let emojis: ElementPosition[] = Array(numOfEmojis) .fill(emoji) .map((emoji) => { let x = randomBetween(0, w - 1); let y = randomBetween(-42, -600); let size = randomBetween(32, 60); return new ElementPosition(emoji, x, y, size, size); }); let frame = window.requestAnimationFrame(function draw() { const bodyBackground = window.getComputedStyle( document.body ).backgroundColor; emojis.forEach((element) => { element.updateY( interpolate(element.y, { min: -42, max: h }, { min: 8, max: 1 }) ); }); emojis = emojis.filter((elem) => { if (elem.y > h + 42) { return false; } return true; }); ctx.fillStyle = bodyBackground; ctx.fillRect(0, 0, w - 1, h - 1); emojis.forEach((elem) => { const alpha = interpolate( elem.y, { min: 0, max: (h / 3) * 2 }, { min: 1, max: 0 } ); ctx.fillStyle = `rgb(17, 24, 39, ${alpha})`; ctx.font = elem.width + "px sans"; ctx.fillText(elem.emoji, elem.x, elem.y); }); frame = window.requestAnimationFrame(draw); }); return () => { ctx.fillStyle = bodyBackground; ctx.fillRect(0, 0, w - 1, h - 1); window.cancelAnimationFrame(frame); }; }, [emoji]); useEffect(() => { function handleEmoji(e: CustomEvent) { setEmoji(e.detail); clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => { setEmoji(null); }, 5000); } window.addEventListener(CUSTOM_EVENT_NAME, handleEmoji); return () => window.removeEventListener(CUSTOM_EVENT_NAME, handleEmoji); }, []); return ; } export function rainEmoji(emoji: string) { const event = new CustomEvent(CUSTOM_EVENT_NAME, { detail: emoji }); window.dispatchEvent(event); } interface RangeValue { min: number; max: number; } function interpolate(current: number, from: RangeValue, to: RangeValue) { if (current < from.min) { return to.min; } if (current > from.max) { return to.max; } // y-y1/y2-y1 = x-x1/x2-x1 // y = x-x1/x2-x1*y2-y1 + y1 return ( ((current - from.min) / (from.max - from.min)) * (to.max - to.min) + to.min ); } function randomBetween(min: number, max: number) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } class ElementPosition { emoji: string; x: number; y: number; width: number; height: number; velocity: number; constructor( emoji: string, x: number, y: number, width: number, height: number ) { this.emoji = emoji; this.x = x; this.y = y; this.width = width; this.height = height; } updateY(velocity: number) { this.y = this.y + velocity; } } ================================================ FILE: components/EmojiSelector.tsx ================================================ import { Menu, MenuButton, MenuList, MenuItem } from "@reach/menu-button"; import { memo } from "react"; interface Props { onSendEmoji: (emoji: string) => void; } function EmojiSelector(props: Props) { return ( ); } export default memo(EmojiSelector); function EmojiBar({ onSelect }) { const emojis = ["🎉", "🥳", "😂", "😭", "❤️", "🤬"]; return ( {emojis.map((emoji) => ( onSelect(emoji)} > {emoji} ))} ); } ================================================ FILE: components/Header.tsx ================================================ import Head from "next/head"; import dynamic from "next/dynamic"; import React, { ReactNode } from "react"; const EmojiSelector = dynamic(() => import("./EmojiSelector"), { ssr: false }); interface Props { title?: string; description?: string; keywords?: string[]; ogImage?: string; customHeading?: ReactNode; warnStorageDisabled?: boolean; isLiveMode?: boolean; themeColor?: string; showLiarOption?: boolean; onShowStats?: () => void; onShowHelp?: () => void; onShowSettings?: () => void; onSendEmoji?: (emoji: string) => void; path?: string; } export default function Header(props: Props) { const { title = "Katla - Permainan Tebak Kata | 1 Hari 1 Kata 6 Kesempatan", description = "Tebak kata rahasia dalam 6 percobaan. Kata baru tersedia setiap hari.", keywords = [ "game", "permainan", "main", "tebak", "kata", "rahasia", "sembunyi", "clue", "petunjuk", "wordle", "bahasa", "indonesia", "karya", "anak", "bangsa", "kbbi", ], ogImage = "https://katla.vercel.app/og.png", customHeading, onShowStats, onShowHelp, onShowSettings, onSendEmoji, warnStorageDisabled, isLiveMode, themeColor = "#15803D", showLiarOption, path = "/", } = props; return ( ); } ================================================ FILE: components/HeadingWithNum.tsx ================================================ interface Props { num: string | number | null; enableLiarMode?: boolean; } export default function HeadingWithNum(props: Props) { const [_, mm, dd] = new Date().toISOString().split("T")[0].split("-"); const isIndonesiaIndependenceDay = mm === "08" && dd === "17"; let customNumClass = ""; if (isIndonesiaIndependenceDay) { customNumClass = "text-white bg-red-500 p-1"; } return ( {props.enableLiarMode ? "Katlie" : <>Katla} {props.num && ( #{props.num} )} ); } ================================================ FILE: components/HelpModal.tsx ================================================ import { memo } from "react"; import Modal from "./Modal"; import Tile from "./Tile"; interface Props { isOpen: boolean; onClose: () => void; isLiveMode?: boolean; } export default memo(HelpModal); function HelpModal(props: Props) { const { isOpen, onClose, isLiveMode = false } = props; return ( Cara Bermain

Tebak Katla dalam 6 kesempatan. {isLiveMode ? (

Mainkan bersama teman-temanmu (maksimum 10 orang). Setelah permainan selesai, babak baru akan otomatis dimulai dalam 5 detik.

) : ( 1 hari ada 1 kata rahasia. )}

Setiap tebakan harus merupakan kata valid 5 huruf sesuai KBBI. Tekan tombol ENTER untuk mengirimkan jawaban

Setelah jawaban dikirimkan, warna kotak akan berubah untuk menunjukkan seberapa dekat tebakanmu dari kata rahasia


Contoh
{"semua".split("").map((char, i) => { return ( ); })}
Huruf S ada dan posisinya sudah tepat
{"kasur".split("").map((char, i) => { return ( ); })}
Huruf A ada namun posisinya belum tepat
{"duduk".split("").map((char, i) => { return ( ); })}
Tidak ada huruf K di kata rahasia

{isLiveMode ? null : (

Akan ada Katla baru setiap hari!

)}
); } ================================================ FILE: components/Keyboard.tsx ================================================ import { MutableRefObject, useEffect, useRef } from "react"; import KeyboardButton from "./KeyboardButton"; import { AnswerState, GameState } from "../utils/types"; import { decode } from "../utils/codec"; interface Props { onPressChar: (char: string) => void; onBackspace: () => void; onSubmit: () => void; gameState: GameState; hash: string; isAnimating: MutableRefObject; } export default function Keyboard(props: Props) { const { onPressChar, onBackspace, onSubmit, gameState, hash, isAnimating } = props; const answer = decode(hash); const usedChars = new Set( gameState.answers .slice(0, gameState.attempt) .map((answer) => answer.split("")) .flat() ); const correctChars = new Set(); gameState.answers.forEach((userAnswer, i) => { if (i < gameState.attempt) { userAnswer.split("").forEach((char, j) => { if (answer[j] === char) { correctChars.add(char); } }); } }); function getKeyboardState(char: string): AnswerState { let state = null; if (correctChars.has(char)) { state = "c"; } else if (usedChars.has(char) && answer.includes(char)) { state = "e"; } else if (usedChars.has(char)) { state = "w"; } return gameState.enableLiarMode ? null : state; } const pressed = useRef(null); useEffect(() => { function handleKeydown(e: KeyboardEvent) { if (gameState.attempt === 6) { return; } const currentText = gameState.answers[gameState.attempt]; if ( pressed.current === true && e.key === currentText[currentText.length - 1] ) { return; } if (isAnimating.current) { return; } pressed.current = true; if (e.key === "Backspace") { onBackspace(); } else if (e.key === "Enter") { // prevent modal to be opened when pressing enter e.preventDefault(); onSubmit(); } else if (gameState.enableFreeEdit && e.key === "_") { onPressChar(e.key); } else if (/[a-z]/i.test(e.key) && e.key.length === 1) { onPressChar(e.key); } } function handleKeyup() { pressed.current = false; } document.addEventListener("keydown", handleKeydown); document.addEventListener("keyup", handleKeyup); return () => { document.removeEventListener("keydown", handleKeydown); document.removeEventListener("keyup", handleKeyup); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [gameState]); return (
{"qwertyuiop".split("").map((char) => ( onPressChar(char)} > {char} ))}
{"asdfghjkl".split("").map((char) => ( onPressChar(char)} > {char} ))}
Enter {"zxcvbnm".split("").map((char) => ( onPressChar(char)} > {char} ))} {gameState.enableFreeEdit && ( onPressChar("_")} scale={1.5} > _ )}
); } ================================================ FILE: components/KeyboardButton.tsx ================================================ import { ComponentProps, memo } from "react"; import { AnswerState } from "../utils/types"; type Props = { state: AnswerState; scale?: number; } & Omit, "className" | "style">; export default function KeyboardButton(props: Props) { let color = "bg-gray-300 text-gray-900 dark:bg-gray-500 dark:text-gray-200"; switch (props.state) { case "c": color = "text-white bg-correct"; break; case "e": color = "text-white bg-exist"; break; case "w": color = "text-white bg-gray-500 dark:text-gray-200 dark:bg-gray-700"; break; default: } return ( {children} ); } const Title = ({ children }) => (

{children}

); Modal.Title = Title; type ModalState = "help" | "stats" | "settings"; type ModalStateReturn = [ModalState, (state: ModalState) => void, () => void]; export function useModalState(game: Game, stats: GameStats): ModalStateReturn { const [modalState, setModalState] = useState(null); useEffect(() => { if (!game.ready) { return; } // show help screen for first-time player if ( getTotalPlay(stats) === 0 && game.state.attempt === 0 && game.state.answers[0] === "" ) { setModalState("help"); } // show stats screen if user already finished playing current session else if (isGameFinished(game)) { setModalState("stats"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [game.ready]); useEffect(() => { // reset modal every new game setModalState(null); }, [game.num]); const resetModalState = useCallback(() => { setModalState(null); }, []); return [modalState, setModalState, resetModalState]; } ================================================ FILE: components/SettingsModal.tsx ================================================ import { useTheme } from "next-themes"; import Link from "./Link"; import Modal from "./Modal"; import Alert from "./Alert"; import { GAME_STATE_KEY, GAME_STATS_KEY, INVALID_WORDS_KEY, LAST_HASH_KEY, LAST_SESSION_RESET_KEY, } from "../utils/constants"; import LocalStorage from "../utils/browser"; import { ForcedResult, Game, LiveConfig } from "../utils/types"; import { shareInviteLink } from "../utils/liveGame"; interface Props { isOpen: boolean; onClose: () => void; game: Game; liveConfig?: LiveConfig; } export default function SettingsModal(props: Props) { const { game, isOpen, onClose, liveConfig } = props; const { resolvedTheme, setTheme } = useTheme(); const showLiarMode = game.num === 71; const correctColor = game.state.enableHighContrast ? "biru" : "hijau"; const incorrectColor = game.state.enableHighContrast ? "oranye" : "kuning"; return ( Pengaturan {game.num !== -1 && ( { game.setState({ ...game.state, enableHardMode }); }} /> )} { setTheme(active ? "dark" : "light"); }} /> { game.setState({ ...game.state, enableHighContrast }); }} /> { game.setState({ ...game.state, enableFreeEdit }); }} /> {showLiarMode && ( { game.setState({ ...game.state, enableLiarMode, lieBoxes: generateLieBoxes(), }); }} /> )} {liveConfig ? ( liveConfig.isHost ? ( ) : ( ) ) : ( )} ); } interface AdminToolsProps { game: Game; onClose: () => void; config: LiveConfig; } function AdminTools(props: AdminToolsProps) { const { game, onClose, config } = props; function handleReset() { game.resetState(); onClose(); } function handleInvite() { shareInviteLink(config, onClose); } return (

Mode Lawan

); } interface PlayerToolsProps { config: LiveConfig; } function PlayerTools(props: PlayerToolsProps) { const { config } = props; function handleInvite() { shareInviteLink(config); } return (

Pengaturan Mode Lawan

); } function AdditionalInformation() { function handleReset() { LocalStorage.removeItem(GAME_STATE_KEY); LocalStorage.removeItem(GAME_STATS_KEY); LocalStorage.removeItem(INVALID_WORDS_KEY); LocalStorage.removeItem(LAST_HASH_KEY); LocalStorage.setItem( LAST_SESSION_RESET_KEY, new Date().getTime().toString() ); window.location.reload(); } return ( <>

Informasi

Katla merupakan imitasi adaptasi dari{" "} Wordle

Kamu bisa melihat daftar kata yang telah digunakan sebelumnya di dalam{" "} Arsip

Terdapat Masalah?

Bantuan atau
); } interface SwitchProps { active: boolean; title: string; subtitle?: string; onlyEnableOnFirstAttempt?: boolean; attempt?: number; isExperimental?: boolean; onChange: (active: boolean) => void; } function Switch(props: SwitchProps) { const { title, subtitle, isExperimental, active, onChange, onlyEnableOnFirstAttempt, attempt, } = props; const disabled = onlyEnableOnFirstAttempt && attempt > 0; const disabledText = `${title} hanya dapat diganti di awal permainan`; const warningText = onlyEnableOnFirstAttempt ? `Hanya dapat diganti di awal permainan` : isExperimental ? "Masih dalam tahap uji coba" : ""; function handleClick() { if (disabled) { Alert.show(disabledText, { id: "disabled" }); return; } onChange(!active); } return (

{title}{" "} {warningText && ( {"(" + warningText + ")"} )}

{subtitle && {subtitle}}
); } function generateLieBoxes(): ForcedResult[] { // only 5 because we want the last one to show the real answer return Array(5) .fill(0) .map(() => { const isExist = Math.random() > 0.35; const isCorrect = Math.random() > 0.5; const col = Math.floor(Math.random() * 5); return isExist ? (isCorrect ? [col, "c"] : [col, "e"]) : [col, "w"]; }); } ================================================ FILE: components/SponsorshipFooter.tsx ================================================ export default function SponsorshipFooter() { return ( ); } ================================================ FILE: components/StatsModal.tsx ================================================ import useSWR from "swr"; import { LegacyRef, useEffect, useRef, useState } from "react"; import { useTheme } from "next-themes"; import Modal from "./Modal"; import { AnswerState, Game, GameStats } from "../utils/types"; import { decode } from "../utils/codec"; import fetcher from "../utils/fetcher"; import { pad0 } from "../utils/formatter"; import { useRemainingTime, getTotalPlay, getTotalWin, getAnswerStates, } from "../utils/game"; import { checkNativeShareSupport, shareText } from "../utils/browser"; import { isEidMessage } from "../utils/message"; interface Props { isOpen: boolean; onClose: () => void; game: Game; stats: GameStats; remainingTime?: ReturnType; } declare global { interface Window { adsbygoogle: { loaded: boolean; push: (v: unknown) => void }; } } const GRAPH_WIDTH_MIN_RATIO = 10; export default function StatsModal(props: Props) { const { isOpen, onClose, game, stats } = props; const { resolvedTheme } = useTheme(); const isAnswered = game.state.answers[game.state.attempt - 1] === decode(game.hash); const [canShareImage, setCanShareImage] = useState(false); const [showAnswersCheckbox, setShowAnswersCheckbox] = useState(false); useEffect(() => { const canShareImage = checkNativeShareSupport() && typeof navigator.canShare === "function"; setCanShareImage(canShareImage); if (!canShareImage) { // preload image share logic import("file-saver"); } }, []); const answer = decode(game.hash); const showShare = game.state.attempt === 6 || game.state.answers[game.state.attempt - 1] === answer; const totalWin = getTotalWin(stats); const totalPlay = getTotalPlay(stats); const title = game.state.enableLiarMode ? "Katlie" : "Katla"; function generateText() { const hardModeMarker = game.state.enableHardMode ? "*" : ""; const score = game.state.answers[game.state.attempt - 1] === answer ? game.state.attempt : "X"; let text = `${title} ${game.num} ${score}/6${hardModeMarker}\n\n`; game.state.answers.filter(Boolean).forEach((userAnswer) => { const answerEmojis = getAnswerStates(userAnswer, answer).map((state) => { switch (state) { case "c": return game.state.enableHighContrast ? "🟧" : "🟩"; case "e": return game.state.enableHighContrast ? "🟦" : "🟨"; case "w": return resolvedTheme === "dark" ? "⬛" : "⬜️"; } }); text += `${answerEmojis.join("")}\n`; }); if ( game.num === 102 && isEidMessage(game.state.answers[game.state.attempt - 1]) ) { text = `🙏🙏🙏🙏🙏\n\n`; text += `⬛⬛🟩⬛⬛\n`; text += `⬛🟩🟨🟩⬛\n`; text += `🟩🟨🟩🟨🟩\n`; text += `⬛🟩🟨🟩⬛\n`; text += `⬛⬛🟩⬛⬛\n`; text += "\n" + window.location.href; return text; } text += "\n" + window.location.href; return text; } function handleShare() { shareText(generateText(), { cb: onClose }); } async function handleShareImage() { const canvas = document.createElement("canvas"); canvas.height = 1400; canvas.width = 900; const ctx = canvas.getContext("2d"); ctx.fillStyle = resolvedTheme === "dark" ? "#111827" : "#ffffff"; ctx.fillRect(0, 0, canvas.width, canvas.height); const gap = 10; const paddingH = 200; const paddingT = 400; const size = (canvas.width - paddingH * 2 - gap * 4) / 5; const score = game.state.answers[game.state.attempt - 1] === answer ? game.state.attempt : "X"; const hardModeMarker = game.state.enableHardMode ? "*" : ""; let answers = game.state.answers.slice(0, game.state.attempt); let answerStates: AnswerState[][] = answers.map((answer) => { return getAnswerStates(answer, decode(game.hash)); }); let text = `${title} ${game.num} ${score}/6${hardModeMarker}\n\n`; if ( game.num === 102 && isEidMessage(game.state.answers[game.state.attempt - 1]) ) { text = `🙏🙏🙏🙏🙏🙏`; answers = ["mohon", "maaf", "lahir", "dan", "batin"]; answerStates = [ ["w", "w", "c", "w", "w"], ["w", "c", "e", "c", "w"], ["c", "e", "c", "e", "c"], ["w", "c", "e", "c", "w"], ["w", "w", "c", "w", "w"], ]; } ctx.font = "bold 42px sans-serif"; ctx.textAlign = "center"; ctx.fillStyle = resolvedTheme === "dark" ? "#ffffff" : "#111827"; ctx.fillText(text, canvas.width / 2, 300); ctx.font = "32px sans-serif"; ctx.fillText("katla.vercel.app", canvas.width / 2, canvas.height - 150); answerStates.forEach((states, y) => { const answer = answers[y]; states.forEach((state, x) => { if (state === "c") { ctx.fillStyle = game.state.enableHighContrast ? "#f5793a" : "#15803d"; } else if (state === "e") { ctx.fillStyle = game.state.enableHighContrast ? "#85c0f9" : "#ca8a04"; } else { ctx.fillStyle = resolvedTheme === "dark" ? "#374151" : "#6b7280"; } const marginH = gap * x; const marginV = gap * y; const rectX = paddingH + x * size + marginH; const rectY = paddingT + y * size + marginV; ctx.beginPath(); ctx.rect(rectX, rectY, size, size); ctx.fill(); if (showAnswersCheckbox) { ctx.font = "bold 54px sans-serif"; ctx.fillStyle = "#ffffff"; ctx.fillText( answer[x]?.toUpperCase() ?? "", rectX + size / 2, rectY + size / 1.5 ); } }); }); const dataURL = canvas.toDataURL(); const blob = await (await fetch(dataURL)).blob(); if (canShareImage) { const imageName = showAnswersCheckbox ? `katla-${game.num}-with-answers.jpg` : `katla-${game.num}.jpg`; const shareData = { files: [ new File([blob], imageName, { type: "image/jpeg", lastModified: new Date().getTime(), }), ], }; navigator.share(shareData).catch(() => {}); } else { const { saveAs } = await import("file-saver").then((mod) => mod.default); const imageName = showAnswersCheckbox ? `katla-${game.num}-with-answers` : `katla-${game.num}`; saveAs(blob, imageName); } } function handleShareToTwitter() { const text = generateText(); const encodeURI = text.replace(/\n/g, "%0A"); const shareToTwitter = `https://twitter.com/intent/tweet?text=${encodeURI}`; window.open(shareToTwitter, "_blank"); } const { fail: _, ...distribution } = stats.distribution; const maxDistribution = Math.max(...Object.values(distribution)); const [isAdUnitRendered, setIsAdUnitRendered] = useState(false); const adsByGooglePushedRef = useRef(false); const adUnitRef = useRef(null); useEffect(() => { if (!isOpen || !isAdUnitRendered || adsByGooglePushedRef.current) return; const adUnit = adUnitRef.current; adUnit.innerHTML = ` `; try { // @ts-ignore window.adsbygoogle = (window.adsbygoogle || []).push({}); adsByGooglePushedRef.current = true; } catch (err) { // ignore } }, [isOpen, isAdUnitRendered]); const adUnitRefCallback: LegacyRef = (element) => { adUnitRef.current = element; setIsAdUnitRendered(!!element); }; return ( Statistik
{totalPlay}
Dimainkan
{totalPlay === 0 ? 0 : Math.round((totalWin / totalPlay) * 100)}
% Menang
{stats.currentStreak}
Runtunan saat ini
{stats.maxStreak}
Runtunan maksimum

Distribusi Tebakan

{Array(6) .fill("") .map((_, i) => { const shouldHighlight = isAnswered && i === game.state.attempt - 1; const ratio = totalWin === 0 ? GRAPH_WIDTH_MIN_RATIO : Math.max( (Number(stats.distribution[i + 1]) / maxDistribution) * 100, GRAPH_WIDTH_MIN_RATIO ); const alignment = ratio === GRAPH_WIDTH_MIN_RATIO ? "justify-center" : "justify-end"; const background = shouldHighlight ? "bg-accent" : "bg-gray-500"; return (
{i + 1}
{stats.distribution[i + 1]}
); })}
{showShare && ( <>
{props.remainingTime ? ( ) : (
)}
)} ); } function WordDefinition({ answer }) { const { data = [] } = useSWR(`/api/define/${answer}`, (path) => { return fetcher(path, { headers: { Authorization: `token ${process.env.NEXT_PUBLIC_DEFINE_TOKEN}`, }, }); }); return (

Katla hari ini

Mohon untuk tetap dirahasiakan, semua orang mendapatkan kata yang sama 🙏

{answer} {data.length > 0 ? ( data.length === 1 ? ( `: ${data[0]}` ) : (

    {data.map((d, i) => (
  • {d}
  • ))}
) ) : null}

Lihat di KBBI
); } function TimeCounter({ time }: { time: ReturnType }) { const remainingTime = `${time.hours}:${pad0(time.minutes)}:${pad0( time.seconds )}`; return (
Katla berikutnya
{remainingTime}
); } ================================================ FILE: components/Tile.tsx ================================================ import { CSSProperties, useEffect, useState } from "react"; import { FLIP_ANIMATION_DELAY_MS, FLIP_ANIMATION_DURATION_MS, SHAKE_ANIMATION_DURATION_MS, } from "../utils/constants"; import { AnswerState } from "../utils/types"; interface Props { char: string; state: AnswerState; isInvalid?: boolean; delay: number; onPress?: () => void; } export default function Tile(props: Props) { const [background, setBackground] = useState("text-gray-700 dark:text-white"); const [animate, setAnimationEnabled] = useState(false); const border = props.char === " " ? "border" : props.state === null ? "border-3" : ""; const borderColor = props.char === " " ? "dark:border-gray-700 border-gray-400" : props.state === null ? "border-gray-500" : ""; useEffect(() => { if (props.state === null) { return; } setAnimationEnabled(true); () => setAnimationEnabled(false); }, [props.state]); const style: CSSProperties = {}; if (props.isInvalid) { style.animationName = "shake"; style.animationDuration = `${SHAKE_ANIMATION_DURATION_MS}ms`; } if (animate) { style.animationName = "flip"; style.animationDuration = `${FLIP_ANIMATION_DURATION_MS}ms`; style.animationDelay = `${props.delay}ms`; } useEffect(() => { setTimeout(() => { switch (props.state) { case "c": setBackground("text-white dark:text-gray-200 bg-correct"); break; case "e": setBackground("text-white bg-exist"); break; case "w": setBackground( "text-white bg-gray-500 dark:text-gray-200 dark:bg-gray-700" ); break; } }, props.delay + FLIP_ANIMATION_DELAY_MS); }, [animate, props.state, props.delay]); return ( ); } ================================================ FILE: env.dev ================================================ NEXT_PUBLIC_DEFINE_TOKEN="test" ================================================ FILE: jest.config.js ================================================ /* * For a detailed explanation regarding each configuration property and type check, visit: * https://jestjs.io/docs/configuration */ module.exports = { // All imported modules in your tests should be mocked automatically // automock: false, // Stop running tests after `n` failures // bail: 0, // The directory where Jest should store its cached dependency information // cacheDirectory: "/private/var/folders/s9/v3c_29w146qcg2v7f_pwmb3c0000gn/T/jest_dx", // Automatically clear mock calls, instances and results before every test clearMocks: true, // Indicates whether the coverage information should be collected while executing the test // collectCoverage: false, // An array of glob patterns indicating a set of files for which coverage information should be collected // collectCoverageFrom: undefined, // The directory where Jest should output its coverage files // coverageDirectory: undefined, // An array of regexp pattern strings used to skip coverage collection // coveragePathIgnorePatterns: [ // "/node_modules/" // ], // Indicates which provider should be used to instrument code for coverage coverageProvider: "v8", // A list of reporter names that Jest uses when writing coverage reports // coverageReporters: [ // "json", // "text", // "lcov", // "clover" // ], // An object that configures minimum threshold enforcement for coverage results // coverageThreshold: undefined, // A path to a custom dependency extractor // dependencyExtractor: undefined, // Make calling deprecated APIs throw helpful error messages // errorOnDeprecated: false, // Force coverage collection from ignored files using an array of glob patterns // forceCoverageMatch: [], // A path to a module which exports an async function that is triggered once before all test suites // globalSetup: undefined, // A path to a module which exports an async function that is triggered once after all test suites // globalTeardown: undefined, // A set of global variables that need to be available in all test environments // globals: {}, // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. // maxWorkers: "50%", // An array of directory names to be searched recursively up from the requiring module's location // moduleDirectories: [ // "node_modules" // ], // An array of file extensions your modules use // moduleFileExtensions: [ // "js", // "jsx", // "ts", // "tsx", // "json", // "node" // ], // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module // moduleNameMapper: {}, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // modulePathIgnorePatterns: [], // Activates notifications for test results // notify: false, // An enum that specifies notification mode. Requires { notify: true } // notifyMode: "failure-change", // A preset that is used as a base for Jest's configuration // preset: undefined, // Run tests from one or more projects // projects: undefined, // Use this configuration option to add custom reporters to Jest // reporters: undefined, // Automatically reset mock state before every test // resetMocks: false, // Reset the module registry before running each individual test // resetModules: false, // A path to a custom resolver // resolver: undefined, // Automatically restore mock state and implementation before every test // restoreMocks: false, // The root directory that Jest should scan for tests and modules within // rootDir: undefined, // A list of paths to directories that Jest should use to search for files in // roots: [ // "" // ], // Allows you to use a custom runner instead of Jest's default test runner // runner: "jest-runner", // The paths to modules that run some code to configure or set up the testing environment before each test // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test // setupFilesAfterEnv: [], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, // A list of paths to snapshot serializer modules Jest should use for snapshot testing // snapshotSerializers: [], // The test environment that will be used for testing // testEnvironment: "jest-environment-node", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, // Adds a location field to test results // testLocationInResults: false, // The glob patterns Jest uses to detect test files // testMatch: [ // "**/__tests__/**/*.[jt]s?(x)", // "**/?(*.)+(spec|test).[tj]s?(x)" // ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // testPathIgnorePatterns: [ // "/node_modules/" // ], // The regexp pattern or array of patterns that Jest uses to detect test files // testRegex: [], // This option allows the use of a custom results processor // testResultsProcessor: undefined, // This option allows use of a custom test runner // testRunner: "jest-circus/runner", // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href // testURL: "http://localhost", // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" // timers: "real", // A map from regular expressions to paths to transformers transform: { "^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }], }, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // transformIgnorePatterns: [ // "/node_modules/", // "\\.pnp\\.[^\\/]+$" // ], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, // Indicates whether each individual test should be reported during the run // verbose: undefined, // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode // watchPathIgnorePatterns: [], // Whether to use watchman for file crawling // watchman: true, }; ================================================ FILE: next-env.d.ts ================================================ /// /// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. ================================================ FILE: next.config.js ================================================ const { withSentryConfig } = require("@sentry/nextjs"); const nextConfig = { reactStrictMode: true, }; const sentryWebpackPluginOptions = { silent: true, }; module.exports = withSentryConfig(nextConfig, sentryWebpackPluginOptions); ================================================ FILE: package.json ================================================ { "name": "katla", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "test": "jest", "update": "node ./.scripts/update.js", "format": "prettier --write '**/*.{js,md,json,yml,yaml,css,ts,tsx}'", "prepare": "husky install" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" }, "dependencies": { "@liveblocks/client": "^0.14.1", "@liveblocks/node": "^0.3.0", "@liveblocks/react": "^0.14.1", "@reach/dialog": "^0.16.2", "@reach/menu-button": "^0.16.2", "@sentry/nextjs": "^6.17.4", "@supabase/supabase-js": "^1.30.7", "canvas-confetti": "^1.4.0", "cheerio": "^1.0.0-rc.10", "date-fns": "^2.28.0", "file-saver": "^2.0.5", "next": "^12.1.0", "next-themes": "^0.0.16", "react": "17.0.2", "react-dom": "17.0.2", "react-hot-toast": "^2.2.0", "swr": "^1.1.2" }, "devDependencies": { "@octokit/rest": "^18.12.0", "@testing-library/react-hooks": "^7.0.2", "@types/react": "^17.0.38", "autoprefixer": "^10.4.2", "eslint": "8.7.0", "eslint-config-next": "12.1.0", "husky": "^7.0.4", "jest": "^27.4.7", "lint-staged": "^12.2.2", "postcss": "^8.4.5", "prettier": "^2.5.1", "tailwindcss": "^3.0.15", "typescript": "^4.5.4" } } ================================================ FILE: pages/_app.tsx ================================================ import Script from "next/script"; import "../styles/globals.css"; import { ThemeProvider } from "next-themes"; import Alert from "../components/Alert"; import EmojiRain from "../components/EmojiRain"; export default function MyApp({ Component, pageProps }) { return (