Upload files to "Examm"
This commit is contained in:
parent
9515197b7b
commit
b15ea71de5
10
Examm/Dockerfile
Normal file
10
Examm/Dockerfile
Normal 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
120
Examm/README.md
Normal 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
21
Examm/deployment.yaml
Normal 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
43
Examm/index.html
Normal 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
4
Examm/namespace.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: typing-game
|
11
Examm/postgres-service.yaml
Normal file
11
Examm/postgres-service.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: typing-game
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
39
Examm/postgres-statefulset.yaml
Normal file
39
Examm/postgres-statefulset.yaml
Normal 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
13
Examm/prepare-app.sh
Normal 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
25
Examm/pv-pvc.yaml
Normal 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
114
Examm/script.js
Normal 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
14
Examm/service.yaml
Normal 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
11
Examm/start-app.sh
Normal 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
31
Examm/statefulset.yaml
Normal 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
11
Examm/stop-app.sh
Normal 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
93
Examm/style.css
Normal 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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user