repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
stars
latest
clone command
git clone gitlawb://did:key:z6MkvfHn...poLu/gitcatgit clone gitlawb://did:key:z6MkvfHn.../gitcata815108csync from playground1d ago| #1 | import { useState, useEffect, useCallback, useRef } from 'react'; |
| #2 | import PopCat from './PopCat'; |
| #3 | import Leaderboard from './Leaderboard'; |
| #4 | import TipButton from './TipButton'; |
| #5 | import { usePopSound } from './usePopSound'; |
| #6 | import { useParticles } from './useParticles'; |
| #7 | |
| #8 | const STORAGE_KEY = 'gitcat_highscore'; |
| #9 | |
| #10 | export default function App() { |
| #11 | const [count, setCount] = useState(0); |
| #12 | const [highScore, setHighScore] = useState(() => { |
| #13 | const saved = localStorage.getItem(STORAGE_KEY); |
| #14 | return saved ? parseInt(saved, 10) : 0; |
| #15 | }); |
| #16 | const [isOpen, setIsOpen] = useState(false); |
| #17 | const [combo, setCombo] = useState(0); |
| #18 | const [showRestart, setShowRestart] = useState(false); |
| #19 | |
| #20 | const comboRef = useRef(0); |
| #21 | const highScoreRef = useRef(highScore); |
| #22 | const comboTimerRef = useRef(0); |
| #23 | const catRef = useRef<HTMLDivElement>(null); |
| #24 | const countRef = useRef<HTMLDivElement>(null); |
| #25 | const playPop = usePopSound(); |
| #26 | const { canvasRef, spawn } = useParticles(); |
| #27 | |
| #28 | useEffect(() => { highScoreRef.current = highScore; }, [highScore]); |
| #29 | |
| #30 | const handlePop = useCallback(() => { |
| #31 | setCount((prev) => { |
| #32 | const next = prev + 1; |
| #33 | if (next > highScoreRef.current) { |
| #34 | highScoreRef.current = next; |
| #35 | setHighScore(next); |
| #36 | localStorage.setItem(STORAGE_KEY, String(next)); |
| #37 | } |
| #38 | return next; |
| #39 | }); |
| #40 | |
| #41 | setIsOpen(true); |
| #42 | |
| #43 | // Pop number animation |
| #44 | if (countRef.current) { |
| #45 | countRef.current.style.transform = 'scale(1.15)'; |
| #46 | requestAnimationFrame(() => { |
| #47 | requestAnimationFrame(() => { |
| #48 | if (countRef.current) { |
| #49 | countRef.current.style.transform = 'scale(1)'; |
| #50 | } |
| #51 | }); |
| #52 | }); |
| #53 | } |
| #54 | |
| #55 | // Combo tracking |
| #56 | clearTimeout(comboTimerRef.current); |
| #57 | comboRef.current += 1; |
| #58 | setCombo(comboRef.current); |
| #59 | comboTimerRef.current = window.setTimeout(() => { |
| #60 | comboRef.current = 0; |
| #61 | setCombo(0); |
| #62 | }, 800); |
| #63 | |
| #64 | // Spawn particles |
| #65 | if (catRef.current) { |
| #66 | const rect = catRef.current.getBoundingClientRect(); |
| #67 | const cx = rect.left + rect.width / 2; |
| #68 | const cy = rect.top + rect.height / 2; |
| #69 | const intensity = Math.min(comboRef.current / 10, 3) + 1; |
| #70 | const particleCount = Math.min(6 + Math.floor(comboRef.current / 3), 24); |
| #71 | spawn(cx, cy, particleCount, intensity); |
| #72 | } |
| #73 | |
| #74 | // Reset mouth |
| #75 | setTimeout(() => { |
| #76 | setIsOpen(false); |
| #77 | }, 100); |
| #78 | |
| #79 | playPop(); |
| #80 | }, [playPop, spawn]); |
| #81 | |
| #82 | // Keyboard |
| #83 | useEffect(() => { |
| #84 | const onKey = (e: KeyboardEvent) => { |
| #85 | if (e.key === ' ' || e.key === 'g' || e.key === 'G') { |
| #86 | e.preventDefault(); |
| #87 | handlePop(); |
| #88 | } |
| #89 | }; |
| #90 | window.addEventListener('keydown', onKey); |
| #91 | return () => window.removeEventListener('keydown', onKey); |
| #92 | }, [handlePop]); |
| #93 | |
| #94 | const handleReset = () => { |
| #95 | setCount(0); |
| #96 | setCombo(0); |
| #97 | comboRef.current = 0; |
| #98 | setShowRestart(false); |
| #99 | setHighScore(0); |
| #100 | highScoreRef.current = 0; |
| #101 | localStorage.setItem(STORAGE_KEY, '0'); |
| #102 | }; |
| #103 | |
| #104 | return ( |
| #105 | <div className="app"> |
| #106 | <canvas ref={canvasRef} className="particles-canvas" /> |
| #107 | |
| #108 | <a |
| #109 | className="brand-link" |
| #110 | href="https://x.com/Abilahbr" |
| #111 | target="_blank" |
| #112 | rel="noopener noreferrer" |
| #113 | > |
| #114 | <img |
| #115 | className="brand-avatar" |
| #116 | src="https://pbs.twimg.com/profile_images/1986706433703088128/kIturs-a.jpg" |
| #117 | alt="@Abilahbr" |
| #118 | width={28} |
| #119 | height={28} |
| #120 | /> |
| #121 | <span className="brand-handle">@Abilahbr</span> |
| #122 | </a> |
| #123 | |
| #124 | <div className="top-bar"> |
| #125 | <h1 className="title">GITCAT</h1> |
| #126 | <div className="score-box"> |
| #127 | <div className="score-label">Git Pops</div> |
| #128 | <div className="score-value">{highScore.toLocaleString()}</div> |
| #129 | </div> |
| #130 | </div> |
| #131 | |
| #132 | |
| #133 | <div className="main-area"> |
| #134 | {combo >= 5 && ( |
| #135 | <div className="combo-badge" key={combo}> |
| #136 | {combo}x COMBO{combo >= 20 ? '!!!' : combo >= 10 ? '!!' : '!'} |
| #137 | </div> |
| #138 | )} |
| #139 | |
| #140 | <div |
| #141 | ref={countRef} |
| #142 | className="pop-count" |
| #143 | style={{ transition: 'transform 0.08s ease-out' }} |
| #144 | > |
| #145 | {count.toLocaleString()} |
| #146 | </div> |
| #147 | |
| #148 | <div |
| #149 | ref={catRef} |
| #150 | className={`cat-container ${isOpen ? 'cat-bounce' : ''}`} |
| #151 | onClick={handlePop} |
| #152 | onTouchStart={(e) => { |
| #153 | e.preventDefault(); |
| #154 | handlePop(); |
| #155 | }} |
| #156 | > |
| #157 | <div className={`cat-glow ${isOpen ? 'glow-active' : ''}`} /> |
| #158 | <PopCat isOpen={isOpen} /> |
| #159 | </div> |
| #160 | |
| #161 | <div className="hint">Click or press Space / G to pop!</div> |
| #162 | </div> |
| #163 | |
| #164 | <Leaderboard playerCount={count} /> |
| #165 | |
| #166 | <TipButton /> |
| #167 | |
| #168 | {showRestart ? ( |
| #169 | <div className="restart-panel"> |
| #170 | <p>Reset your score?</p> |
| #171 | <button className="restart-btn" onClick={handleReset}> |
| #172 | Confirm Reset |
| #173 | </button> |
| #174 | <button className="restart-btn cancel" onClick={() => setShowRestart(false)}> |
| #175 | Cancel |
| #176 | </button> |
| #177 | </div> |
| #178 | ) : ( |
| #179 | <button |
| #180 | className="restart-trigger" |
| #181 | onClick={() => setShowRestart(true)} |
| #182 | > |
| #183 | ↺ Reset |
| #184 | </button> |
| #185 | )} |
| #186 | |
| #187 | </div> |
| #188 | ); |
| #189 | } |
| #190 |