Upload files to "/"
This commit is contained in:
parent
72491d78c6
commit
7fa4363cb7
10
Dockerfile
Normal file
10
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
|
126
README.md
126
README.md
@ -1,38 +1,122 @@
|
|||||||
# WebApp Docker Deployment
|
# 🧠 Typing Speed & RSI Awareness Game - Kubernetes Deployment
|
||||||
|
|
||||||
# Requirements -
|
This project showcases a web-based **Typing Speed & RSI Awareness Game** deployed on a Kubernetes cluster. Although the game is a simple frontend app (HTML, CSS, JavaScript), the Kubernetes setup is fully compliant with deployment best practices and assignment requirements.
|
||||||
|
|
||||||
Docker installed
|
---
|
||||||
|
|
||||||
Docker Compose (optional)
|
## 🎮 Game Overview
|
||||||
|
|
||||||
# Description -
|
The application is designed to:
|
||||||
This application consists of a web service and a database service. The web service runs on an Apache server with PHP and connects to a MySQL database.
|
- Improve typing speed and accuracy.
|
||||||
|
- Educate users on RSI (Repetitive Strain Injury) prevention.
|
||||||
|
- Include real-time feedback and typing stats.
|
||||||
|
- Encourage healthy typing habits with periodic stretch reminders.
|
||||||
|
|
||||||
# Networks and Volumes -
|
---
|
||||||
my_network: A bridge network for inter-container communication.
|
|
||||||
|
|
||||||
# mysql_data -
|
## 📁 Project Structure
|
||||||
|
|
||||||
A volume to persist database data.
|
File/Folder Description |
|
||||||
|
|
||||||
# Container Configuration
|
| index.html`, `style.css`, `script.js` | Frontend game files |
|
||||||
apache: Web service running Apache with PHP, exposes port 8080.
|
| Dockerfile` | Builds Nginx image to serve the game |
|
||||||
|
| prepare-app.sh` | Prepares the environment and Docker image |
|
||||||
|
| start-app.sh` | Applies all Kubernetes resources |
|
||||||
|
| stop-app.sh` | Cleans up all Kubernetes resources |
|
||||||
|
| namespace.yaml` | Creates the `typing-game` namespace |
|
||||||
|
| deployment.yaml` | Deploys the frontend via Nginx |
|
||||||
|
| service.yaml` | Exposes frontend using NodePort |
|
||||||
|
| pv-pvc.yaml` | Defines persistent volume and claim |
|
||||||
|
| statefulset.yaml` | Dummy backend with StatefulSet + PVC |
|
||||||
|
|
|
||||||
|
|
||||||
# mysql -
|
---
|
||||||
|
|
||||||
MySQL database service using the official MySQL image, exposes port 3306.
|
## How to Run
|
||||||
|
|
||||||
# Instructions
|
### Step 1: Prepare the Application
|
||||||
|
|
||||||
Prepare the application: ./prepare-app.sh
|
```bash
|
||||||
|
./prepare-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
Start the application: ./start-app.sh
|
- Builds the Docker image: `typing-game`
|
||||||
|
- Saves it as a `.tar` for loading on cluster nodes
|
||||||
|
- Prepares persistent storage
|
||||||
|
|
||||||
Stop the application: ./stop-app.sh
|
---
|
||||||
|
|
||||||
Remove the application: ./remove-app.sh
|
### Step 2: Deploy the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This applies:
|
||||||
|
- Namespace
|
||||||
|
- Deployment
|
||||||
|
- Service
|
||||||
|
- StatefulSet
|
||||||
|
- PersistentVolume + PersistentVolumeClaim
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: Access the App
|
||||||
|
|
||||||
|
Open in your browser:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://<your-node-ip>:30007
|
||||||
|
```
|
||||||
|
|
||||||
|
> Replace `<your-node-ip>` with the external IP of your Kubernetes node.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4: Clean Up
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./stop-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This deletes all Kubernetes resources created by this project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Assignment Requirements Fulfilled
|
||||||
|
|
||||||
|
### 1. Namespace
|
||||||
|
**Requirement:** All objects must belong to a custom namespace.
|
||||||
|
**You Did:** Defined `typing-game` in `namespace.yaml`, and referenced it in all other files.
|
||||||
|
|
||||||
|
### 2. Deployment
|
||||||
|
**Requirement:** Deploy your web application.
|
||||||
|
**You Did:** Used `deployment.yaml` to deploy your frontend game using Nginx.
|
||||||
|
|
||||||
|
### 3. StatefulSet + PersistentVolume + PersistentVolumeClaim
|
||||||
|
**Requirement:** Demonstrate persistent storage via a StatefulSet.
|
||||||
|
**You Did:**
|
||||||
|
- Used `statefulset.yaml` with Alpine Linux writing logs to `/data/log.txt`.
|
||||||
|
- Mounted `/data` using a PVC created from `pv-pvc.yaml`.
|
||||||
|
|
||||||
|
### 4. Service
|
||||||
|
**Requirement:** Expose your app.
|
||||||
|
**You Did:** Used a `NodePort` service in `service.yaml` on port `30007`.
|
||||||
|
|
||||||
|
### 5. start-app.sh
|
||||||
|
**Requirement:** Script to create all objects.
|
||||||
|
**You Did:** Automated creation of namespace, Deployment, Service, StatefulSet, PV/PVC.
|
||||||
|
|
||||||
|
### 6. stop-app.sh
|
||||||
|
**Requirement:** Script to delete all objects.
|
||||||
|
**You Did:** Cleanly removes all created Kubernetes objects.
|
||||||
|
|
||||||
|
### 7. prepare-app.sh
|
||||||
|
**Requirement:** Prepares the environment.
|
||||||
|
**You Did:** Builds and saves Docker image, prepares persistent volume.
|
||||||
|
|
||||||
|
### 8. Dockerfile
|
||||||
|
**Requirement:** Provide necessary container files.
|
||||||
|
**You Did:** Dockerfile builds a custom Nginx image that serves your HTML/JS/CSS game files.
|
||||||
|
|
||||||
# Accessing the Application
|
|
||||||
|
|
||||||
Open a web browser and navigate to http://localhost:8080.
|
|
21
deployment.yaml
Normal file
21
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
index.html
Normal file
43
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
namespace.yaml
Normal file
4
namespace.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: typing-game
|
15
prepare-app.sh
Executable file → Normal file
15
prepare-app.sh
Executable file → Normal file
@ -1,6 +1,13 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo "Preparing application..."
|
|
||||||
docker network create myapp-net
|
|
||||||
docker volume create mysql-data
|
|
||||||
echo "Preparation complete."
|
|
||||||
|
|
||||||
|
# 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
pv-pvc.yaml
Normal file
25
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
script.js
Normal file
114
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
service.yaml
Normal file
14
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
|
12
start-app.sh
Executable file → Normal file
12
start-app.sh
Executable file → Normal file
@ -1,5 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo "Starting application..."
|
|
||||||
docker compose up -d
|
|
||||||
echo "Application started. Access it at http://localhost:8080"
|
|
||||||
|
|
||||||
|
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
statefulset.yaml
Normal file
31
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
|
12
stop-app.sh
Executable file → Normal file
12
stop-app.sh
Executable file → Normal file
@ -1,5 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
echo "Stopping application..."
|
|
||||||
docker-compose down
|
|
||||||
echo "Application stopped."
|
|
||||||
|
|
||||||
|
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
style.css
Normal file
93
style.css
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
background: #f0f4f8;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
text-align: center;
|
||||||
|
background: white;
|
||||||
|
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