Upload files to "Examm"

This commit is contained in:
Puneet Khurana 2025-04-29 18:54:32 +00:00
parent 9515197b7b
commit b15ea71de5
15 changed files with 560 additions and 0 deletions

10
Examm/Dockerfile Normal file
View File

@ -0,0 +1,10 @@
# Use the official nginx image
FROM nginx:alpine
# Copy the game files into the nginx html folder
COPY index.html /usr/share/nginx/html/
COPY style.css /usr/share/nginx/html/
COPY script.js /usr/share/nginx/html/
# Expose port 80
EXPOSE 80

120
Examm/README.md Normal file
View File

@ -0,0 +1,120 @@
# Typing Speed & RSI Awareness Game - Kubernetes & Netlify Deployment
This project showcases a web-based Typing Speed & RSI Awareness Game, deployed in two ways:
1. Locally on a Kubernetes cluster using best practices and persistent storage.
2. Published online using Netlify, making it accessible via a global CDN.
# Game Overview
The application is designed to:
1. Improve typing speed and accuracy.
2. Educate users on RSI (Repetitive Strain Injury) prevention.
3. Include real-time feedback and typing stats.
4. Encourage healthy typing habits with periodic stretch reminders.
Project Structure
File/Folder - Description
index.html, style.css, script.js - Frontend files for the typing game
Dockerfile - Builds an Nginx image to serve the game
prepare-app.sh - Prepares Docker image and volume for Kubernetes
start-app.sh - Deploys app using Kubernetes objects
stop-app.sh - Deletes all Kubernetes resources
namespace.yaml - Creates typing-game namespace
deployment.yaml - Deploys frontend with Nginx
service.yaml
pv-pvc.yaml - Persistent Volume and Claim configuration
statefulset.yaml
# Namespace
Created and referenced typing-game in all YAML files.
# Deployment
Used deployment.yaml for Nginx-based frontend.
# tatefulSet + Persistent Volume
statefulset.yaml logs data persistently via PVC.
# Service
Exposed frontend via NodePort (port 30007).
# Scripts
start-app.sh - to deploy, stop-app.sh to clean up.
prepare-app.sh - for Docker image and volume setup.
./stop-app.sh - Which will stop all Kubernetes resources associated with the app.
Dockerfile - Built custom image serving game with Nginx.
# How to Deploy on Kubernetes
Step 1: Prepare the Application (All the files and scripts)
Step 2: ./prepare-app.sh
Step 3: Deploy the App via - ./start-app.sh
Which Applies:
A)Namespace
B)Deployment
C)Service
D)StatefulSet
E)PersistentVolume and PVC
Step 4: Access the App (Locally)
Replace <your-ip> with the IP address of your Kubernetes node.
Step 5: Clean Up via - ./stop-app.sh
Which will stop all Kubernetes resources associated with the app.
# How to Deploy with Netlify (Online)
You also hosted the Typing Game on Netlify, a popular platform for deploying Games or websites on Public Cloud Environment
# Setup Steps (Completed)
1. Installed Netlify CLI using Homebrew:
2. brew install netlify-cli
3. Logged in and linked your project:
4. netlify login
5. netlify init # Or `netlify link` if project already exists
Your game is now live at:
https://typing-game-app.netlify.app
# Netlify Publish Directory
When prompted during deploy, you provided this directory:
/Users/puneetkhurana/Documents/zkt25/Examm
# CI-Like Workflow with CLI
To update your live website via CLI:
Make changes in your local Examm folder.
netlify deploy --prod
Changes will go live on the same URL.
This allows you to re-deploy instantly after edits without needing to use the Netlify UI.
# Summary
1. Typing Game promotes speed and RSI awareness.
2. Deployed on Kubernetes with storage and services.
3. Published live via Netlify using CLI.
4. Fully automated with scripts for ease of use and reusability.

21
Examm/deployment.yaml Normal file
View File

@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: typing-game-deployment
namespace: typing-game
spec:
replicas: 1
selector:
matchLabels:
app: typing-game
template:
metadata:
labels:
app: typing-game
spec:
containers:
- name: typing-game
image: typing-game:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80

43
Examm/index.html Normal file
View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Typing Speed & RSI Awareness Game</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Fast Fingers, Healthy Habits</h1>
<p style="font-style: italic; margin-top: -5px; color: #555;">
Type fast. Type smart. Stay safe.
</p>
<div class="game-container">
<button id="dark-mode-toggle">🌙 Toggle Dark Mode</button>
<div id="session-timer">You've been typing for 0 minutes.</div>
<div id="high-score">🏆 Highest Score: 0</div>
<h1>Typing Speed & RSI Awareness</h1>
<div id="word-display">Start</div>
<input type="text" id="word-input" placeholder="Type here..." autofocus />
<div class="stats">
<span id="score">Score: 0</span>
<span id="accuracy">Accuracy: 100%</span>
<span id="timer">Time: 0:00</span>
</div>
<div id="rsi-tip" class="tip-box">💡 Keep your wrists straight while typing.</div>
<div id="stretch-popup" class="stretch-popup"></div>
<div class="gif-container">
<img id="stretch-gif"
src="https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZHpoc3hwOTQ4Nm0xcWowN2h3ODhudXdybnYzdWJrZ2JiamNkajU3eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/395GQQavHm5CPXboEG/giphy.gif"
alt="Stretch your hands!"
style="display: none; width: 140px; border-radius: 12px;" />
</div>
</div>
<script src="script.js"></script>
</body>
</html>

4
Examm/namespace.yaml Normal file
View File

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: typing-game

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: typing-game
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432

View File

@ -0,0 +1,39 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: typing-game
spec:
serviceName: "postgres"
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:13
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: typingdb
- name: POSTGRES_USER
value: typinguser
- name: POSTGRES_PASSWORD
value: secret123
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-storage
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi

13
Examm/prepare-app.sh Normal file
View File

@ -0,0 +1,13 @@
#!/bin/bash
# Create Docker image
echo "Building Docker image: typing-game"
docker build -t typing-game .
# Save Docker image as tar to simulate deployment to remote cluster
docker save typing-game > typing-game.tar
# Dummy command to create persistent volume directory (for demo)
mkdir -p ./persistent-data
echo "Preparation complete."

25
Examm/pv-pvc.yaml Normal file
View File

@ -0,0 +1,25 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: typing-game-pv
namespace: typing-game
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/data/typing-game"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: typing-game-pvc
namespace: typing-game
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

114
Examm/script.js Normal file
View File

@ -0,0 +1,114 @@
const words = ["apple", "banana", "cherry", "dragon", "elephant", "fish", "grape", "house", "ink", "jungle"];
let currentWord = "";
let score = 0;
let typedWords = 0;
let missed = 0;
let totalTyped = 0;
let startTime = Date.now();
let wordStartTime = Date.now();
const wordDisplay = document.getElementById("word-display");
const wordInput = document.getElementById("word-input");
const scoreDisplay = document.getElementById("score");
const accuracyDisplay = document.getElementById("accuracy");
const timerDisplay = document.getElementById("timer");
const tipBox = document.getElementById("rsi-tip");
const stretchPopup = document.getElementById("stretch-popup");
const sessionTimerText = document.getElementById("session-timer");
const highScoreDisplay = document.getElementById("high-score");
const darkModeToggle = document.getElementById("dark-mode-toggle");
const tips = [
"💡 Keep your wrists straight while typing.",
"🪑 Sit with your back supported.",
"👁 Adjust your screen to eye level.",
"💧 Take a short water break.",
"🧍‍♂️ Stand and stretch for a moment."
];
let highScore = localStorage.getItem("typingHighScore") || 0;
highScoreDisplay.textContent = `🏆 Highest Score: ${highScore}`;
function generateWord() {
currentWord = words[Math.floor(Math.random() * words.length)];
wordDisplay.textContent = currentWord;
wordInput.value = "";
wordStartTime = Date.now();
}
function updateScore() {
scoreDisplay.textContent = `Score: ${score}`;
const accuracy = totalTyped === 0 ? 100 : Math.round((score / totalTyped) * 100);
accuracyDisplay.textContent = `Accuracy: ${accuracy}%`;
}
function updateTimer() {
const now = Date.now();
const seconds = Math.floor((now - startTime) / 1000);
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
timerDisplay.textContent = `Time: ${mins}:${secs.toString().padStart(2, '0')}`;
}
function gameOver() {
if (score > highScore) {
localStorage.setItem("typingHighScore", score);
alert(`New High Score: ${score}! 🎉`);
} else {
alert("Game Over! You missed 3 words.");
}
location.reload();
}
wordInput.addEventListener("input", () => {
if (wordInput.value.trim() === currentWord) {
score++;
typedWords++;
totalTyped++;
generateWord();
updateScore();
if (typedWords % 10 === 0) {
stretchPopup.textContent = "🧘‍♀️ Time to stretch your fingers or roll your shoulders!";
document.getElementById("stretch-gif").style.display = "block";
setTimeout(() => {
stretchPopup.textContent = "";
document.getElementById("stretch-gif").style.display = "none";
}, 5000);
}
}
});
wordInput.addEventListener("blur", () => wordInput.focus());
setInterval(() => {
const timeSinceWordShown = Date.now() - wordStartTime;
if (timeSinceWordShown >= 8000 && wordInput.value.trim() === "") {
missed++;
totalTyped++;
if (missed >= 3) gameOver();
generateWord();
updateScore();
}
}, 1000);
let tipIndex = 0;
setInterval(() => {
tipBox.textContent = tips[tipIndex];
tipIndex = (tipIndex + 1) % tips.length;
}, 30000);
setInterval(updateTimer, 1000);
setInterval(() => {
const minutes = Math.floor((Date.now() - startTime) / 60000);
sessionTimerText.textContent = `You've been typing for ${minutes} minute${minutes !== 1 ? 's' : ''}.`;
}, 10000);
darkModeToggle.addEventListener("click", () => {
document.body.classList.toggle("dark-mode");
});
generateWord();
updateScore();
updateTimer();

14
Examm/service.yaml Normal file
View File

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: typing-game-service
namespace: typing-game
spec:
type: NodePort
selector:
app: typing-game
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30007

11
Examm/start-app.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
echo "Creating Kubernetes resources..."
kubectl apply -f namespace.yaml
kubectl apply -f pv-pvc.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f statefulset.yaml
echo "Application deployed under namespace 'typing-game'."

31
Examm/statefulset.yaml Normal file
View File

@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: typing-backend
namespace: typing-game
spec:
serviceName: "backend-service"
replicas: 1
selector:
matchLabels:
app: typing-backend
template:
metadata:
labels:
app: typing-backend
spec:
containers:
- name: backend
image: alpine
command: ["/bin/sh", "-c", "while true; do echo Hello from StatefulSet >> /data/log.txt; sleep 10; done"]
volumeMounts:
- name: backend-storage
mountPath: /data
volumeClaimTemplates:
- metadata:
name: backend-storage
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi

11
Examm/stop-app.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
echo "Deleting Kubernetes resources..."
kubectl delete -f statefulset.yaml
kubectl delete -f service.yaml
kubectl delete -f deployment.yaml
kubectl delete -f pv-pvc.yaml
kubectl delete -f namespace.yaml
echo "All resources deleted."

93
Examm/style.css Normal file
View File

@ -0,0 +1,93 @@
body {
font-family: 'Segoe UI', sans-serif;
background: #bbff00;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.game-container {
text-align: center;
background: rgb(0, 249, 253);
padding: 2rem;
border-radius: 12px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
width: 90%;
max-width: 500px;
}
#dark-mode-toggle {
margin-bottom: 10px;
}
#session-timer {
font-size: 0.9rem;
margin-bottom: 5px;
}
#high-score {
font-size: 1rem;
font-weight: bold;
}
.gif-container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
}
#word-display {
font-size: 2rem;
margin-bottom: 1rem;
}
#word-input {
font-size: 1.2rem;
padding: 0.5rem;
border: 2px solid #ccc;
border-radius: 8px;
width: 100%;
}
.stats {
margin-top: 1rem;
display: flex;
justify-content: space-between;
font-size: 1rem;
}
.tip-box {
margin-top: 1rem;
padding: 0.5rem;
background: #e6f7ff;
border-left: 5px solid #1890ff;
border-radius: 6px;
font-size: 0.9rem;
}
.stretch-popup {
margin-top: 1rem;
color: #d48806;
font-weight: bold;
}
body.dark-mode {
background: #1a1a1a;
color: #ffffff;
}
body.dark-mode .game-container {
background: #2c2c2c;
color: #ffffff;
}
body.dark-mode input {
background: #333;
color: #fff;
border: 2px solid #888;
}