diff --git a/Examm/Dockerfile b/Examm/Dockerfile new file mode 100644 index 0000000..6850ed4 --- /dev/null +++ b/Examm/Dockerfile @@ -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 diff --git a/Examm/README.md b/Examm/README.md new file mode 100644 index 0000000..3af69e5 --- /dev/null +++ b/Examm/README.md @@ -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 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. \ No newline at end of file diff --git a/Examm/deployment.yaml b/Examm/deployment.yaml new file mode 100644 index 0000000..a8e8eca --- /dev/null +++ b/Examm/deployment.yaml @@ -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 diff --git a/Examm/index.html b/Examm/index.html new file mode 100644 index 0000000..9241897 --- /dev/null +++ b/Examm/index.html @@ -0,0 +1,43 @@ + + + + + + Typing Speed & RSI Awareness Game + + + +

Fast Fingers, Healthy Habits

+

+ Type fast. Type smart. Stay safe. +

+ +
+ +
You've been typing for 0 minutes.
+
🏆 Highest Score: 0
+ +

Typing Speed & RSI Awareness

+
Start
+ + +
+ Score: 0 + Accuracy: 100% + Time: 0:00 +
+ +
💡 Keep your wrists straight while typing.
+
+ +
+ +
+
+ + + + diff --git a/Examm/namespace.yaml b/Examm/namespace.yaml new file mode 100644 index 0000000..49f25f0 --- /dev/null +++ b/Examm/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: typing-game diff --git a/Examm/postgres-service.yaml b/Examm/postgres-service.yaml new file mode 100644 index 0000000..d8fe1c4 --- /dev/null +++ b/Examm/postgres-service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: typing-game +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 diff --git a/Examm/postgres-statefulset.yaml b/Examm/postgres-statefulset.yaml new file mode 100644 index 0000000..f25edd9 --- /dev/null +++ b/Examm/postgres-statefulset.yaml @@ -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 diff --git a/Examm/prepare-app.sh b/Examm/prepare-app.sh new file mode 100644 index 0000000..d1860c5 --- /dev/null +++ b/Examm/prepare-app.sh @@ -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." diff --git a/Examm/pv-pvc.yaml b/Examm/pv-pvc.yaml new file mode 100644 index 0000000..f976dbb --- /dev/null +++ b/Examm/pv-pvc.yaml @@ -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 diff --git a/Examm/script.js b/Examm/script.js new file mode 100644 index 0000000..be43404 --- /dev/null +++ b/Examm/script.js @@ -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(); diff --git a/Examm/service.yaml b/Examm/service.yaml new file mode 100644 index 0000000..0bb7f38 --- /dev/null +++ b/Examm/service.yaml @@ -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 diff --git a/Examm/start-app.sh b/Examm/start-app.sh new file mode 100644 index 0000000..b003f69 --- /dev/null +++ b/Examm/start-app.sh @@ -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'." diff --git a/Examm/statefulset.yaml b/Examm/statefulset.yaml new file mode 100644 index 0000000..e632822 --- /dev/null +++ b/Examm/statefulset.yaml @@ -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 diff --git a/Examm/stop-app.sh b/Examm/stop-app.sh new file mode 100644 index 0000000..78c1d17 --- /dev/null +++ b/Examm/stop-app.sh @@ -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." diff --git a/Examm/style.css b/Examm/style.css new file mode 100644 index 0000000..214212a --- /dev/null +++ b/Examm/style.css @@ -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; + } + \ No newline at end of file