commit 4cefc00b64a4191697b75d2614a2f8bfe0c2d454 Author: Sistav <212277883+Sistav@users.noreply.github.com> Date: Sun Mar 1 23:10:24 2026 -0500 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9cda046 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules/ +client/node_modules/ +client/build/ +server/monkey.db +server/.env +.git/ +.DS_Store +*.log diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0baafea --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Dependencies +node_modules/ +client/node_modules/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# Environment variables +.env + +# Build outputs +client/build/ +dist/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ + +package-lock.json +/client/node_modules +/client/node_modules/.cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8da32cd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Stage 1: Build the React client +FROM node:20-alpine AS builder +WORKDIR /app/client +COPY client/package*.json ./ +RUN npm install +COPY client/ ./ +RUN npm run build + +# Stage 2: Run the server +FROM node:20-alpine +WORKDIR /app/server +COPY server/package*.json ./ +RUN npm install +COPY server/ ./ +COPY --from=builder /app/client/build ./public +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..8b6c269 --- /dev/null +++ b/client/package.json @@ -0,0 +1,34 @@ +{ + "name": "monkey-typewriter-client", + "version": "1.0.0", + "private": true, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "react-scripts": "5.0.1", + "cross-env": "^7.0.3" + }, + "scripts": { + "start": "cross-env PORT=3001 react-scripts start", + "build": "react-scripts build" + }, + "eslintConfig": { + "extends": [ + "react-app" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "proxy": "http://localhost:3000" +} \ No newline at end of file diff --git a/client/public/assets/Monkey/Air/air.gif b/client/public/assets/Monkey/Air/air.gif new file mode 100644 index 0000000..7c6f530 Binary files /dev/null and b/client/public/assets/Monkey/Air/air.gif differ diff --git a/client/public/assets/Monkey/Air/air0.png b/client/public/assets/Monkey/Air/air0.png new file mode 100644 index 0000000..7ca4aa2 Binary files /dev/null and b/client/public/assets/Monkey/Air/air0.png differ diff --git a/client/public/assets/Monkey/Air/air1.png b/client/public/assets/Monkey/Air/air1.png new file mode 100644 index 0000000..1662f6e Binary files /dev/null and b/client/public/assets/Monkey/Air/air1.png differ diff --git a/client/public/assets/Monkey/Air/air2.png b/client/public/assets/Monkey/Air/air2.png new file mode 100644 index 0000000..16d1919 Binary files /dev/null and b/client/public/assets/Monkey/Air/air2.png differ diff --git a/client/public/assets/Monkey/Air/air3.png b/client/public/assets/Monkey/Air/air3.png new file mode 100644 index 0000000..a6360e9 Binary files /dev/null and b/client/public/assets/Monkey/Air/air3.png differ diff --git a/client/public/assets/Monkey/Air/air4.png b/client/public/assets/Monkey/Air/air4.png new file mode 100644 index 0000000..4f9033d Binary files /dev/null and b/client/public/assets/Monkey/Air/air4.png differ diff --git a/client/public/assets/Monkey/Idle/idle.gif b/client/public/assets/Monkey/Idle/idle.gif new file mode 100644 index 0000000..8867b49 Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle.gif differ diff --git a/client/public/assets/Monkey/Idle/idle0.png b/client/public/assets/Monkey/Idle/idle0.png new file mode 100644 index 0000000..15832c1 Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle0.png differ diff --git a/client/public/assets/Monkey/Idle/idle1.png b/client/public/assets/Monkey/Idle/idle1.png new file mode 100644 index 0000000..6b35d62 Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle1.png differ diff --git a/client/public/assets/Monkey/Idle/idle2.png b/client/public/assets/Monkey/Idle/idle2.png new file mode 100644 index 0000000..c5e356a Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle2.png differ diff --git a/client/public/assets/Monkey/Idle/idle3.png b/client/public/assets/Monkey/Idle/idle3.png new file mode 100644 index 0000000..54ce7fb Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle3.png differ diff --git a/client/public/assets/Monkey/Idle/idle4.png b/client/public/assets/Monkey/Idle/idle4.png new file mode 100644 index 0000000..8241497 Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle4.png differ diff --git a/client/public/assets/Monkey/Idle/idle5.png b/client/public/assets/Monkey/Idle/idle5.png new file mode 100644 index 0000000..8ab2ea2 Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle5.png differ diff --git a/client/public/assets/Monkey/Idle/idle6.png b/client/public/assets/Monkey/Idle/idle6.png new file mode 100644 index 0000000..32d040b Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle6.png differ diff --git a/client/public/assets/Monkey/Idle/idle7.png b/client/public/assets/Monkey/Idle/idle7.png new file mode 100644 index 0000000..316f363 Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle7.png differ diff --git a/client/public/assets/Monkey/Idle/idle8.png b/client/public/assets/Monkey/Idle/idle8.png new file mode 100644 index 0000000..f906783 Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle8.png differ diff --git a/client/public/assets/Monkey/Idle/idle9.png b/client/public/assets/Monkey/Idle/idle9.png new file mode 100644 index 0000000..b0803ac Binary files /dev/null and b/client/public/assets/Monkey/Idle/idle9.png differ diff --git a/client/public/assets/Monkey/Punch/punch.gif b/client/public/assets/Monkey/Punch/punch.gif new file mode 100644 index 0000000..81b0edc Binary files /dev/null and b/client/public/assets/Monkey/Punch/punch.gif differ diff --git a/client/public/assets/Monkey/Punch/punch0.png b/client/public/assets/Monkey/Punch/punch0.png new file mode 100644 index 0000000..8f7454b Binary files /dev/null and b/client/public/assets/Monkey/Punch/punch0.png differ diff --git a/client/public/assets/Monkey/Punch/punch1.png b/client/public/assets/Monkey/Punch/punch1.png new file mode 100644 index 0000000..f3295f2 Binary files /dev/null and b/client/public/assets/Monkey/Punch/punch1.png differ diff --git a/client/public/assets/Monkey/Punch/punch2.png b/client/public/assets/Monkey/Punch/punch2.png new file mode 100644 index 0000000..b0ccc08 Binary files /dev/null and b/client/public/assets/Monkey/Punch/punch2.png differ diff --git a/client/public/assets/Monkey/Punch/punch3.png b/client/public/assets/Monkey/Punch/punch3.png new file mode 100644 index 0000000..53c6021 Binary files /dev/null and b/client/public/assets/Monkey/Punch/punch3.png differ diff --git a/client/public/assets/Monkey/head.png b/client/public/assets/Monkey/head.png new file mode 100644 index 0000000..f76ba73 Binary files /dev/null and b/client/public/assets/Monkey/head.png differ diff --git a/client/public/assets/Typewriter/typewriter.png b/client/public/assets/Typewriter/typewriter.png new file mode 100644 index 0000000..e7fbb50 Binary files /dev/null and b/client/public/assets/Typewriter/typewriter.png differ diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000..7b40de7 --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,11 @@ + + + + + + Monkey Typewriter + + +
+ + \ No newline at end of file diff --git a/client/src/App.css b/client/src/App.css new file mode 100644 index 0000000..6064a55 --- /dev/null +++ b/client/src/App.css @@ -0,0 +1,134 @@ +@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&family=Special+Elite&display=swap"); + +* { + box-sizing: border-box; +} + +body { + font-family: "Special Elite", system-ui; + background-color: #111; + color: #f0f0f0; + margin: 0; + padding: 0; +} + +a { + color: #ccc; +} + +a:hover { + color: #fff; +} + +/* Monkey + typewriter — sits inline in the header, not fixed */ +.monkey-widget { + display: flex; + align-items: flex-end; + flex-shrink: 0; +} + +/* Wrapper handles the positional offset so the shake animation is clean */ +.monkey-widget .typewriter-wrapper { + position: relative; + transform: translate(18px, -8px); + z-index: 2; +} + +.monkey-widget .typewriter-img { + width: 150px; + filter: grayscale(1); + display: block; +} + +@keyframes typewriter-shake { + 0%, 100% { transform: rotate(0deg) translate(0, 0); } + 20% { transform: rotate(-4deg) translate(-3px, -1px); } + 50% { transform: rotate(4deg) translate(3px, 2px); } + 80% { transform: rotate(-2deg) translate(-2px, 0); } +} + +.monkey-widget .typewriter-img.shaking { + animation: typewriter-shake 0.3s ease; +} + +.home-typewriter-shake { + animation: typewriter-shake 0.5s ease; +} + +/* Flying letter spat out of the typewriter */ +@keyframes fly-out { + 0% { opacity: 1; transform: translate(-50%, -50%) translate(0px, 0px); } + 100% { opacity: 0; transform: translate(-50%, -50%) translate(var(--dx), var(--dy)); } +} + +.flying-letter { + position: absolute; + top: 35%; + left: 50%; + pointer-events: none; + animation: fly-out 0.8s ease-out forwards; + font-family: "Special Elite", system-ui; + font-size: 16px; + font-weight: bold; + color: #f0f0f0; + white-space: nowrap; + z-index: 10; +} + +.monkey-widget .monkey-img { + margin-left: -15px; + position: relative; + z-index: 1; +} + +/* Navbar desktop links */ +.navbar-links { + display: flex; + gap: 28px; + align-items: center; +} + +/* Hamburger button — hidden on desktop */ +.navbar-hamburger { + display: none; + background: transparent; + border: none; + color: #f0f0f0; + font-size: 1.4em; + cursor: pointer; + padding: 4px 8px; + line-height: 1; +} + +/* Mobile dropdown menu */ +.navbar-mobile-menu { + display: none; + flex-direction: column; + border-top: 1px solid #222; +} + +@media (max-width: 600px) { + .navbar-links { + display: none; + } + + .navbar-hamburger { + display: block; + } + + .navbar-mobile-menu { + display: flex; + } + + .monkey-widget .typewriter-wrapper { + transform: translate(10px, -5px); + } + + .monkey-widget .typewriter-img { + width: 60px; + } + + .monkey-widget .monkey-img { + width: 90px !important; + } +} diff --git a/client/src/App.js b/client/src/App.js new file mode 100644 index 0000000..7e070a7 --- /dev/null +++ b/client/src/App.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import Navbar from './components/Navbar'; +import Home from './pages/Home'; +import Type from './pages/Type'; +import Leaderboard from './pages/Leaderboard'; +import Credits from './pages/Credits'; + +function App() { + return ( + + + + } /> + } /> + } /> + } /> + + + ); +} + +export default App; diff --git a/client/src/components/MonkeySprite.js b/client/src/components/MonkeySprite.js new file mode 100644 index 0000000..39f47ca --- /dev/null +++ b/client/src/components/MonkeySprite.js @@ -0,0 +1,60 @@ +import React, { useState, useEffect } from 'react'; + +const ANIMATIONS = { + idle: { frames: 10, speed: 80, loop: true, path: '/assets/Monkey/Idle', name: 'idle' }, + air: { frames: 5, speed: 120, loop: false, path: '/assets/Monkey/Air', name: 'air' }, + punch: { frames: 4, speed: 80, loop: false, path: '/assets/Monkey/Punch', name: 'punch'}, +}; + +// Preload all frames once so src swaps are instant (no flicker) +Object.values(ANIMATIONS).forEach(({ path, name, frames }) => { + for (let i = 0; i < frames; i++) { + const img = new Image(); + img.src = `${path}/${name}${i}.png`; + } +}); + +const MonkeySprite = ({ animation = 'idle', onAnimationEnd, className, style: styleProp }) => { + const [frame, setFrame] = useState(0); + const currentAnim = ANIMATIONS[animation]; + + useEffect(() => { + setFrame(0); + let currentFrame = 0; + const interval = setInterval(() => { + currentFrame++; + if (currentFrame >= currentAnim.frames) { + if (currentAnim.loop) { + currentFrame = 0; + setFrame(0); + } else { + clearInterval(interval); + if (onAnimationEnd) onAnimationEnd(); + } + return; + } + setFrame(currentFrame); + }, currentAnim.speed); + + return () => clearInterval(interval); + }, [animation, onAnimationEnd, currentAnim]); + + const src = `${currentAnim.path}/${currentAnim.name}${frame}.png`; + + return ( + + ); +}; + +export default MonkeySprite; diff --git a/client/src/components/Navbar.js b/client/src/components/Navbar.js new file mode 100644 index 0000000..a71fd53 --- /dev/null +++ b/client/src/components/Navbar.js @@ -0,0 +1,90 @@ +import React, { useState } from 'react'; +import { Link, useLocation } from 'react-router-dom'; + +function Navbar() { + const { pathname } = useLocation(); + const [menuOpen, setMenuOpen] = useState(false); + + const linkStyle = (path) => ({ + color: pathname === path ? '#fff' : '#888', + textDecoration: 'none', + fontSize: '1em', + letterSpacing: '0.05em', + borderBottom: pathname === path ? '1px solid #fff' : '1px solid transparent', + padding: '2px 0', + lineHeight: '1.2', + transition: 'color 0.15s', + }); + + const mobileLinkStyle = (path) => ({ + color: pathname === path ? '#fff' : '#888', + textDecoration: 'none', + fontSize: '1.1em', + letterSpacing: '0.05em', + padding: '12px 24px', + borderBottom: '1px solid #222', + display: 'block', + width: '100%', + boxSizing: 'border-box', + transition: 'color 0.15s', + }); + + const closeMenu = () => setMenuOpen(false); + + return ( +