flappy Bird

This commit is contained in:
Kai Ritthaler 2025-06-27 08:59:23 +02:00 committed by Luisa Bellitto
parent 90a4a5fd59
commit 9f9a21818a
13 changed files with 397 additions and 67 deletions

View file

@ -5,7 +5,7 @@ import { Request, Response, NextFunction } from "express";
// Configure multer to store files in memory
const multerInstance = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 }, // Limit file size to 5 MB
limits: { fileSize: 30 * 1024 * 1024 }, // Limit file size to 30 MB
});
export const upload = (req: Request, res: Response, next: NextFunction) => {

View file

@ -4,7 +4,7 @@ import { StatusCodes } from "http-status-codes";
const multerInstance = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
limits: { fileSize: 30 * 1024 * 1024 }, // 30 MB
});
export const upload = (req: Request, res: Response, next: NextFunction) => {

View file

@ -11,7 +11,7 @@ services:
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
test: ["CMD", "pg_isready", "-U", "${DB_USER}"]
interval: 5s
timeout: 3s
retries: 5

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View file

@ -1,6 +1,6 @@
.App {
text-align: center;
min-height: 100vh;
min-height: calc(100vh - var(--header-height));
}
.App-logo {

View file

@ -8,32 +8,31 @@ import Header from "./components/Header";
import Profile from "./pages/Profile";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { Auth } from "./api/Auth";
import { NotFound } from "./pages/404Page/NotFoundPage";
function App() {
return (
<Auth>
<Router>
<div className="App">
<Header />
<Routes>
<Route
path="/login"
element={<LoginAndSignUpPage signupProp={false} />}
></Route>
<Route
path="/register"
element={<LoginAndSignUpPage signupProp={true} />}
></Route>
<Route path="/profile" element={<Profile />}></Route>
</Routes>
<Footer />
</div>
</Router>
</Auth>
);
return (
<Auth>
<Router>
<Header />
<div className="App">
<Routes>
<Route path="*" element={<NotFound />} />
<Route
path="/login"
element={<LoginAndSignUpPage signupProp={false} />}
></Route>
<Route
path="/register"
element={<LoginAndSignUpPage signupProp={true} />}
></Route>
<Route path="/profile" element={<Profile />}></Route>
</Routes>
</div>
<Footer />
</Router>
</Auth>
);
}
export default App;

View file

@ -1,7 +1,7 @@
import axios from "axios";
import { refreshToken } from "./refreshToken";
const excludedUrls: string[] = ["/user/login", "/user/regiser"];
const excludedUrls: string[] = ["/user/login", "/user/register"];
const api = axios.create({
baseURL: "http://localhost:3001/api",

View file

@ -1,59 +1,59 @@
.base-header {
z-index: 10;
width: 100vw;
display: flex;
height: var(--header-height);
justify-content: space-between;
align-items: center;
flex-shrink: 0;
position: sticky;
top: 0;
left: 0;
border-radius: 0rem !important;
z-index: 10;
width: 100vw;
display: flex;
height: var(--header-height);
justify-content: space-between;
align-items: center;
flex-shrink: 0;
position: sticky;
top: 0;
left: 0;
border-radius: 0rem !important;
}
@media only screen and (min-width: 768px) {
.base-header {
height: var(--header-height);
}
.base-header {
height: var(--header-height);
}
}
.header-title {
color: var(--Rotkehlchen-orange-default);
color: var(--Rotkehlchen-orange-default);
}
.header-icon-feather {
height: 35px;
height: 35px;
}
.header-icon-menu {
cursor: pointer;
height: 45px;
cursor: pointer;
height: 45px;
}
.header-icon {
margin: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
@media only screen and (min-width: 768px) {
.header-icon {
margin: 40px;
}
.header-icon {
margin: 40px;
}
}
.drawer-list {
background-color: var(--dark-blue);
min-height: 100vh;
min-width: 13rem;
background-color: var(--dark-blue);
min-height: 100vh;
min-width: 13rem;
}
.drawer-list-item {
color: white;
color: white;
}
.drawer-list-item-button {
height: 10vh;
height: 10vh;
}
.drawer-list-item-button:hover {
background-color: var(--dark-blue-hover);
background-color: var(--dark-blue-hover);
}

View file

@ -0,0 +1,243 @@
import React, { useEffect, useRef, useState } from "react";
import "./notFound.css";
import ButtonPrimary from "../../components/ButtonRotkehlchen";
type Block = {
x: number;
height: number;
passed: boolean;
};
const GAP = 300;
const BLOCK_WIDTH = 60;
const GRAVITY = 0.2;
const FLAP_VELOCITY = -8;
const BIRD_SIZE = 25;
const BLOCK_SPAWN_DISTANCE = 400; // px
export const NotFound = () => {
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const targetRef = useRef<HTMLSpanElement>(null);
const gameRef = useRef<HTMLDivElement>(null);
const [score, setScore] = useState(0);
const [gameOver, setGameOver] = useState(false);
const [hasStarted, setHasStarted] = useState(false);
const [textPos, setTextPos] = useState<number>(screenWidth / 2);
const [rotation, setRotation] = useState(0);
const [renderBlocks, setRenderBlocks] = useState<Block[]>([]);
const [birdPos, setBirdPos] = useState({ x: 0, y: 0 });
const distanceSinceLastBlock = useRef(0);
const speedRef = useRef(2);
const birdPosRef = useRef({ x: 0, y: 0 });
const velocityRef = useRef(0);
const blocksRef = useRef<Block[]>([]);
const scoreRef = useRef(0);
const hasStartedRef = useRef(hasStarted);
const gameOverRef = useRef(gameOver);
const textPosRef = useRef(textPos);
useEffect(() => {
if (targetRef.current && gameRef.current) {
const rect = targetRef.current.getBoundingClientRect();
const gameBox = gameRef.current.getBoundingClientRect();
const yPos = rect.top - gameBox.top;
const x = rect.left + rect.width / 2;
const y = yPos + 15;
birdPosRef.current = { x, y };
setBirdPos({ x, y });
}
}, []);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Space") {
e.preventDefault();
if (!gameOverRef.current) {
setHasStarted(true);
hasStartedRef.current = true;
velocityRef.current = FLAP_VELOCITY;
}
} else if (e.code === "Enter") {
e.preventDefault();
if (gameOverRef.current) {
window.location.reload();
}
} else if (e.code === "KeyR") {
window.location.reload();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
useEffect(() => {
let lastBlockTime = Date.now();
let animationId: number;
function loop() {
if (!hasStartedRef.current || gameOverRef.current) {
setRenderBlocks([...blocksRef.current]);
setBirdPos({ ...birdPosRef.current });
return;
}
velocityRef.current += GRAVITY;
birdPosRef.current.y += velocityRef.current;
setRotation(Math.max(Math.min(velocityRef.current * 2, 90), -15));
textPosRef.current -= speedRef.current;
setTextPos(textPosRef.current);
distanceSinceLastBlock.current += speedRef.current;
if (distanceSinceLastBlock.current >= BLOCK_SPAWN_DISTANCE) {
const blockHeight = Math.random() * (screenHeight - GAP - 100) + 50;
blocksRef.current.push({
x: screenWidth,
height: blockHeight,
passed: false,
});
distanceSinceLastBlock.current = 0;
}
blocksRef.current = blocksRef.current
.map((block) => {
const newX = block.x - speedRef.current;
let passed = block.passed;
if (
newX <= birdPosRef.current.x + BIRD_SIZE &&
newX + BLOCK_WIDTH >= birdPosRef.current.x - BIRD_SIZE
) {
if (
birdPosRef.current.y + BIRD_SIZE >= block.height + GAP ||
birdPosRef.current.y - BIRD_SIZE <= block.height
) {
setGameOver(true);
gameOverRef.current = true;
}
}
if (
!passed &&
newX + BLOCK_WIDTH < birdPosRef.current.x - BIRD_SIZE
) {
scoreRef.current += 1;
speedRef.current += 0.1;
setScore(scoreRef.current);
passed = true;
}
return { ...block, x: newX, passed };
})
.filter((block) => block.x + BLOCK_WIDTH > 0);
if (
birdPosRef.current.y >= screenHeight - BIRD_SIZE ||
birdPosRef.current.y <= 0
) {
setGameOver(true);
gameOverRef.current = true;
}
setRenderBlocks([...blocksRef.current]);
setBirdPos({ ...birdPosRef.current });
if (!gameOverRef.current) {
animationId = requestAnimationFrame(loop);
}
}
if (hasStarted) {
hasStartedRef.current = true;
gameOverRef.current = false;
animationId = requestAnimationFrame(loop);
}
return () => cancelAnimationFrame(animationId);
}, [hasStarted]);
const handleGameClick = () => {
if (gameOver) return;
if (!hasStarted) {
setHasStarted(true);
hasStartedRef.current = true;
velocityRef.current = FLAP_VELOCITY;
} else {
velocityRef.current = FLAP_VELOCITY;
}
};
const birdSprite =
velocityRef.current === 0
? "/assets/images/logoWithoutStick.png"
: velocityRef.current > 0
? "/assets/images/flipp.png"
: "/assets/images/flapp.png";
return (
<div className="game-container" ref={gameRef} onClick={handleGameClick}>
<img
src={birdSprite}
className="bird"
style={{
top: birdPos.y,
left: birdPos.x,
transform: `translate(-50%, -50%) rotate(${rotation}deg)`,
}}
alt="bird"
/>
<div
className="text"
style={{
left: textPos,
}}
>
<h1 key={"404"}>
4<span ref={targetRef}>0</span>4 Not Found
</h1>
</div>
{hasStarted && !gameOver && (
<div className="points body-l">Score: {score}</div>
)}
{renderBlocks.map((block, index) => (
<React.Fragment key={index}>
<div
className="tree-trunk top"
style={{
top: 0,
left: block.x,
width: BLOCK_WIDTH,
height: block.height,
}}
></div>
<div
className="tree-trunk bottom"
style={{
top: block.height + GAP,
left: block.x,
width: BLOCK_WIDTH,
height: screenHeight - block.height - GAP,
}}
></div>
</React.Fragment>
))}
{gameOver && (
<div className="gameOver small-title">
<h1>You have killed the bird</h1>
<p>Your Score is {score}</p>
<ButtonPrimary
style="primary"
label="restart"
type="reset"
onClick={() => window.location.reload()}
/>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,81 @@
.text {
position: absolute;
width: 100%;
top: 50%;
left: 50%;
margin: 0;
transform: translate(-50%, -50%);
color: white;
font-size: 48px;
font-weight: bold;
z-index: 5;
}
.game-container {
width: 100vw;
height: calc(100vh - var(--header-height));
cursor: pointer;
overflow: hidden;
position: relative;
}
.bird {
position: relative;
width: 50px;
height: 50px;
top: 50%;
left: 50%;
transform: translate(0, -10px);
color: black;
display: flex;
align-items: center;
justify-content: center;
}
.points {
position: fixed;
top: 70px;
left: 10px;
background-color: var(--Rotkehlchen-gray);
padding: 5px 10px;
border-radius: 10px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
display: flex;
justify-content: center;
align-items: center;
user-select: none;
pointer-events: none;
z-index: 50;
}
.gameOver {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(13, 10, 56, 0.71);
color: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
text-align: center;
z-index: 1000;
}
.tree-trunk {
width: 60px;
position: absolute;
background: repeating-linear-gradient(
45deg,
#8b5a2b,
#8b5a2b 10px,
#a0522d 10px,
#a0522d 20px
);
box-shadow: inset 0 0 10px #5c4033;
}
.bottom {
border-start-start-radius: 20px;
border-start-end-radius: 20px;
}
.top {
border-end-start-radius: 20px;
border-end-end-radius: 20px;
}

View file

@ -44,12 +44,12 @@ function LoginAndSignUpPage({ signupProp }: { signupProp: boolean }) {
setErrorMessages(undefined);
try {
const response = signup
? await api.post("http://localhost:3001/api/user/register", {
? await api.post("/user/register", {
email: formData.email,
username: formData.username,
password: formData.password,
})
: await api.post("http://localhost:3001/api/user/login", {
: await api.post("/user/login", {
username: formData.username,
password: formData.password,
});
@ -65,7 +65,14 @@ function LoginAndSignUpPage({ signupProp }: { signupProp: boolean }) {
await setUserState();
navigate(returnTo, { replace: true });
} catch (err: any) {
setErrorMessages(err.response.data);
if (err.response?.data) {
setErrorMessages(err.response.data);
} else {
setErrorMessages({
error: "Error",
details: [{ message: err.message || "Something went wrong." }],
});
}
}
};
@ -142,10 +149,10 @@ function LoginAndSignUpPage({ signupProp }: { signupProp: boolean }) {
minLength={signup ? 8 : undefined}
/>
</div>
{errorMessages && (
{errorMessages && errorMessages?.details?.length > 0 && (
<div className="error-messages">
{errorMessages.details.map((detial, index) => (
<p key={index}>{detial.message}</p>
{errorMessages.details.map((detail, index) => (
<p key={index}>{detail.message}</p>
))}
</div>
)}