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 (
+