menambahakn insert database dan testing selenium
This commit is contained in:
parent
17b5eaa5b3
commit
90adfef859
Binary file not shown.
163
backend/app.py
163
backend/app.py
|
@ -1,8 +1,19 @@
|
|||
from flask import Flask, request, jsonify
|
||||
from sklearn.metrics import confusion_matrix, classification_report, accuracy_score
|
||||
from flask_cors import CORS
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
import pandas as pd
|
||||
import os
|
||||
import mysql.connector
|
||||
|
||||
# Konfigurasi koneksi database MySQL
|
||||
DB_CONFIG = {
|
||||
'host': 'localhost',
|
||||
'user': 'root',
|
||||
'password': '',
|
||||
'database': 'game_rating_db'
|
||||
}
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
@ -60,6 +71,42 @@ def add_to_dataset(name, features, rating):
|
|||
data.loc[len(data)] = new_row
|
||||
data.to_excel(DATASET_FILE, index=False)
|
||||
|
||||
# Fungsi untuk menyimpan ke database MySQL
|
||||
def save_rating_to_db(game_name, rating):
|
||||
kategori_map = {
|
||||
"SU": "Semua Umur",
|
||||
"3+": "Untuk usia 3 tahun ke atas",
|
||||
"7+": "Untuk usia 7 tahun ke atas",
|
||||
"13+": "Untuk usia 13 tahun ke atas",
|
||||
"18+": "Untuk usia 18 tahun ke atas"
|
||||
}
|
||||
kategori = kategori_map.get(rating, "Tidak diketahui")
|
||||
|
||||
try:
|
||||
conn = mysql.connector.connect(**DB_CONFIG)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS riwayat (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_game VARCHAR(255),
|
||||
rating VARCHAR(10),
|
||||
kategori VARCHAR(100)
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO riwayat (nama_game, rating, kategori)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (game_name, rating, kategori))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
except mysql.connector.Error as err:
|
||||
print("ERROR saat menyimpan ke database:", err)
|
||||
|
||||
|
||||
# Endpoint untuk mengecek apakah game sudah ada dalam dataset
|
||||
@app.route('/check_game', methods=['POST'])
|
||||
def check_game():
|
||||
|
@ -89,21 +136,21 @@ def submit_answers():
|
|||
answers = data.get('answers', [])
|
||||
|
||||
if not game_name or not answers:
|
||||
print("❌ ERROR: Data tidak lengkap")
|
||||
print("ERROR: Data tidak lengkap")
|
||||
return jsonify({"error": "Data tidak lengkap"}), 400
|
||||
|
||||
print("✔ Nama Game:", game_name)
|
||||
print("✔ Jawaban:", answers)
|
||||
print("Nama Game:", game_name)
|
||||
print("Jawaban:", answers)
|
||||
|
||||
# Cek apakah jawaban valid
|
||||
features = []
|
||||
for answer in answers:
|
||||
if answer not in REVERSE_MAPPING:
|
||||
print(f"❌ ERROR: Jawaban tidak valid -> {answer}")
|
||||
print(f"ERROR: Jawaban tidak valid -> {answer}")
|
||||
return jsonify({"error": f"Jawaban tidak valid: {answer}"}), 400
|
||||
features.append(REVERSE_MAPPING[answer])
|
||||
|
||||
print("✔ Features (dikonversi ke angka):", features)
|
||||
print("Features (dikonversi ke angka):", features)
|
||||
|
||||
|
||||
X, y, dataset = load_dataset()
|
||||
|
@ -111,12 +158,12 @@ def submit_answers():
|
|||
# Jika game sudah ada di dataset, kembalikan rating yang sudah ada
|
||||
if game_name in dataset["Nama Game"].values:
|
||||
existing_rating = dataset.loc[dataset["Nama Game"] == game_name, "Rating"].values[0]
|
||||
print("✔ Game ditemukan dalam dataset. Rating:", existing_rating)
|
||||
print("Game ditemukan dalam dataset. Rating:", existing_rating)
|
||||
return jsonify({'rating': existing_rating})
|
||||
|
||||
# Prediksi rating dengan KNN jika dataset cukup besar
|
||||
if len(X) > 3:
|
||||
knn = KNeighborsClassifier(n_neighbors=3)
|
||||
knn = KNeighborsClassifier(n_neighbors=5)
|
||||
knn.fit(X, y)
|
||||
predicted_rating = knn.predict([features])[0]
|
||||
else:
|
||||
|
@ -132,15 +179,113 @@ def submit_answers():
|
|||
else:
|
||||
predicted_rating = "18+"
|
||||
|
||||
print("✔ Prediksi Rating:", predicted_rating)
|
||||
print("Prediksi Rating:", predicted_rating)
|
||||
|
||||
add_to_dataset(game_name, features, predicted_rating)
|
||||
save_rating_to_db(game_name, predicted_rating)
|
||||
return jsonify({'rating': predicted_rating})
|
||||
|
||||
except Exception as e:
|
||||
print("❌ ERROR di backend:", str(e))
|
||||
print("ERROR di backend:", str(e))
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/autocomplete', methods=['GET'])
|
||||
def autocomplete():
|
||||
try:
|
||||
query = request.args.get('query', '').strip().lower()
|
||||
|
||||
if not query:
|
||||
return jsonify([])
|
||||
|
||||
_, _, dataset = load_dataset()
|
||||
|
||||
# Mencari game yang mengandung teks yang diketik pengguna
|
||||
suggestions = dataset[dataset["Nama Game"].str.lower().str.contains(query, na=False)]["Nama Game"].tolist()
|
||||
|
||||
return jsonify(suggestions)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/latest_games', methods=['GET'])
|
||||
def latest_games():
|
||||
_, _, dataset = load_dataset()
|
||||
|
||||
latest_games = dataset["Nama Game"].dropna().values[-4:].tolist()
|
||||
|
||||
return jsonify(latest_games)
|
||||
|
||||
@app.route('/all_games', methods=['GET'])
|
||||
def all_games():
|
||||
try:
|
||||
_, _, dataset = load_dataset()
|
||||
|
||||
if "Nama Game" in dataset.columns and "Rating" in dataset.columns:
|
||||
games = dataset[["Nama Game", "Rating"]].to_dict(orient="records")
|
||||
return jsonify(games)
|
||||
|
||||
return jsonify({"error": "Kolom Nama Game atau Rating tidak ditemukan"}), 500
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# @app.route('/evaluate_model', methods=['GET'])
|
||||
# def evaluate():
|
||||
# try:
|
||||
# X, y, _ = load_dataset()
|
||||
|
||||
# if len(X) < 2:
|
||||
# return jsonify({"error": "Dataset terlalu kecil untuk evaluasi"}), 400
|
||||
|
||||
# knn = KNeighborsClassifier(n_neighbors=5)
|
||||
# knn.fit(X, y)
|
||||
# y_pred = knn.predict(X)
|
||||
|
||||
# labels = sorted(list(set(y)))
|
||||
# cm = confusion_matrix(y, y_pred, labels=labels)
|
||||
# accuracy = accuracy_score(y, y_pred)
|
||||
|
||||
# # Hitung metrik per kelas secara manual
|
||||
# results = {}
|
||||
# total_TP = total_FP = total_FN = total_TN = 0
|
||||
|
||||
# for i, label in enumerate(labels):
|
||||
# TP = cm[i][i]
|
||||
# FN = sum(cm[i]) - TP
|
||||
# FP = sum(cm[:, i]) - TP
|
||||
# TN = cm.sum() - (TP + FN + FP)
|
||||
|
||||
# recall = TP / (TP + FN) if (TP + FN) else 0
|
||||
# precision = TP / (TP + FP) if (TP + FP) else 0
|
||||
|
||||
# results[label] = {
|
||||
# "TP": int(TP),
|
||||
# "FN": int(FN),
|
||||
# "FP": int(FP),
|
||||
# "TN": int(TN),
|
||||
# "recall": round(float(recall), 4),
|
||||
# "recall_formula": f"{int(TP)} / ({int(TP)} + {int(FN)})",
|
||||
# "precision": round(float(precision), 4),
|
||||
# "precision_formula": f"{int(TP)} / ({int(TP)} + {int(FP)})",
|
||||
# }
|
||||
|
||||
|
||||
# total_TP += TP
|
||||
# total_FP += FP
|
||||
# total_FN += FN
|
||||
# total_TN += TN
|
||||
|
||||
# acc_formula = f"({total_TP} + {total_TN}) / ({total_TP} + {total_TN} + {total_FP} + {total_FN})"
|
||||
# acc_value = (total_TP + total_TN) / (total_TP + total_TN + total_FP + total_FN)
|
||||
|
||||
# return jsonify({
|
||||
# "accuracy": round(float(acc_value), 4),
|
||||
# "accuracy_formula": acc_formula,
|
||||
|
||||
# "per_class_metrics": results
|
||||
# })
|
||||
# except Exception as e:
|
||||
# return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
Binary file not shown.
|
@ -10,6 +10,7 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.2.0",
|
||||
"react-scripts": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -3542,6 +3543,12 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.56.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
|
||||
|
@ -13752,6 +13759,55 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.2.0.tgz",
|
||||
"integrity": "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"turbo-stream": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.2.0.tgz",
|
||||
"integrity": "sha512-cU7lTxETGtQRQbafJubvZKHEn5izNABxZhBY0Jlzdv0gqQhCPQt2J8aN5ZPjS6mQOXn5NnirWNh+FpE8TTYN0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-scripts": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||
|
@ -14654,6 +14710,12 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
|
@ -16020,6 +16082,12 @@
|
|||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
@ -16150,9 +16218,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
|
@ -16160,7 +16228,7 @@
|
|||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"description": "",
|
||||
"dependencies": {
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.2.0",
|
||||
"react-scripts": "^5.0.1"
|
||||
},
|
||||
"browserslist": {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import Header from "./components/Header";
|
||||
import Footer from "./components/Footer";
|
||||
import Tentang from "./components/Tentang";
|
||||
|
@ -39,6 +40,12 @@ const App = () => {
|
|||
const [isPopUpOpen, setIsPopUpOpen] = useState(false);
|
||||
const [isConfirmPopupOpen, setIsConfirmPopupOpen] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState("");
|
||||
const [latestGames, setLatestGames] = useState([]);
|
||||
const [easterEggActive, setEasterEggActive] = useState(false);
|
||||
const homeRef = useRef(null);
|
||||
const tentangRef = useRef(null);
|
||||
const kontakRef = useRef(null);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (gameName.length > 1) {
|
||||
|
@ -51,12 +58,33 @@ const App = () => {
|
|||
}
|
||||
}, [gameName]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("http://127.0.0.1:5000/latest_games")
|
||||
.then(res => res.json())
|
||||
.then(data => setLatestGames(data.slice(-5)))
|
||||
.catch(err => console.error("Error fetching latest games:", err));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.hash) {
|
||||
const section = location.hash.replace("#", "");
|
||||
scrollToSection(section);
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
const handleSelectGame = (selectedGame) => {
|
||||
setGameName(selectedGame);
|
||||
setSuggestions([]);
|
||||
};
|
||||
|
||||
const checkGame = async () => {
|
||||
if (gameName.trim().toLowerCase() === "lulus 2025") {
|
||||
setEasterEggActive(true);
|
||||
setRating("🎉 BISMILLAH LULUS 2025 DENGAN SEDIKIT REVISI 🎓");
|
||||
return;
|
||||
}
|
||||
|
||||
setEasterEggActive(false);
|
||||
const response = await fetch("http://127.0.0.1:5000/check_game", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
@ -70,7 +98,6 @@ const App = () => {
|
|||
setStep(1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOptionClick = (option) => {
|
||||
setSelectedOption(option);
|
||||
setIsPopUpOpen(true);
|
||||
|
@ -97,10 +124,34 @@ const App = () => {
|
|||
setRating(data.rating);
|
||||
};
|
||||
|
||||
const fetchGameRating = async (game) => {
|
||||
const response = await fetch("http://127.0.0.1:5000/check_game", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ game_name: game }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.exists) {
|
||||
setGameName(game);
|
||||
setRating(data.rating);
|
||||
setStep(0);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToSection = (section) => {
|
||||
if (section === "home") {
|
||||
homeRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
} else if (section === "tentang") {
|
||||
tentangRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
} else if (section === "kontak") {
|
||||
kontakRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Header/>
|
||||
<div className="h-screen w-full bg-cover bg-center bg-no-repeat bg-black bg-opacity-50 flex items-center justify-center"
|
||||
<Header scrollToSection={scrollToSection} />
|
||||
<div id="home" ref={homeRef} className="h-screen w-full bg-cover bg-center bg-no-repeat bg-black bg-opacity-50 flex items-center justify-center"
|
||||
style={{ backgroundImage: "url('/images/bgwebrating.png')", backgroundSize: "105%" }}>
|
||||
<div className={`w-full max-w-lg p-6 text-center relative ${step > 0 || rating ? 'bg-white rounded-lg shadow-md' : ''}`}>
|
||||
{step === 0 && !rating && (
|
||||
|
@ -118,7 +169,6 @@ const App = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Tombol merah: Kembali ke input nama game */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
|
@ -129,7 +179,6 @@ const App = () => {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!rating && step === 0 && (
|
||||
<>
|
||||
<p className="mt-4 text-left text-white">Masukkan nama game:</p>
|
||||
|
@ -158,9 +207,30 @@ const App = () => {
|
|||
>
|
||||
Cek
|
||||
</button>
|
||||
{/* Daftar 5 game terbaru */}
|
||||
{latestGames.length > 0 && (
|
||||
<div className="mt-6 px-6 bg-yellowlight p-2 rounded">
|
||||
<h2 className="text-black text-bold text-lg text-left font-semibold mb-2">Game Terbaru:</h2>
|
||||
<div className=" flex flex-wrap max-w-full gap-x-6 gap-y-2 ">
|
||||
{latestGames.map((game, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="text-black text-bold border-b border-gray-400 hover:text-white-300 cursor-pointer text-left whitespace-nowrap"
|
||||
onClick={() => fetchGameRating(game)}
|
||||
>
|
||||
{game}
|
||||
</button>
|
||||
))}
|
||||
{latestGames.length >= 4 && (
|
||||
<a href="/list-games" className="text-blue-800 hover:underline ml-2">
|
||||
Selengkapnya
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!rating && step > 0 && step <= questions.length && (
|
||||
<>
|
||||
<p className="mt-4">{questions[step - 1]}</p>
|
||||
|
@ -175,7 +245,6 @@ const App = () => {
|
|||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!rating && step > questions.length && (
|
||||
<>
|
||||
{/* Menampilkan tabel hasil jawaban */}
|
||||
|
@ -198,7 +267,6 @@ const App = () => {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Tombol kembali ke input game */}
|
||||
<button
|
||||
onClick={() => {
|
||||
|
@ -210,7 +278,6 @@ const App = () => {
|
|||
>
|
||||
Kembali
|
||||
</button>
|
||||
|
||||
{/* Tombol untuk konfirmasi hasil */}
|
||||
<button
|
||||
onClick={submitAnswers}
|
||||
|
@ -222,18 +289,29 @@ const App = () => {
|
|||
)}
|
||||
|
||||
{rating && (
|
||||
<>
|
||||
{easterEggActive ? (
|
||||
<h2 className="text-xl font-bold text-center text-red-600 mt-4">
|
||||
🎉 BISMILLAH LULUS 2025 DENGAN SEDIKIT REVISI 🎓
|
||||
</h2>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-xl font-semibold mt-4">
|
||||
Game <span className="font-bold text-blue-500">{gameName}</span> memiliki rating
|
||||
<span className="font-bold text-red-500"> {rating}</span>
|
||||
</h2>
|
||||
<p className="mt-2"><strong>Penjelasan:</strong> {ratingDescriptions[rating]}</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
setAnswers([]);
|
||||
setRating(null);
|
||||
setGameName("");
|
||||
setEasterEggActive(false);
|
||||
window.location.reload();
|
||||
}}
|
||||
className="bg-red-500 text-white px-4 py-2 rounded mt-4 w-full"
|
||||
>
|
||||
|
@ -242,6 +320,7 @@ const App = () => {
|
|||
</>
|
||||
)}
|
||||
|
||||
|
||||
{/* Pop-up konfirmasi pilihan jawaban */}
|
||||
<PopUp
|
||||
isOpen={isPopUpOpen}
|
||||
|
@ -259,10 +338,14 @@ const App = () => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tentang" ref={tentangRef}>
|
||||
<Tentang />
|
||||
</div>
|
||||
<KategoriRating />
|
||||
<div id="kontak" ref={kontakRef}>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,25 +1,41 @@
|
|||
import react from "react";
|
||||
|
||||
const Header = () => {
|
||||
const Header = ({ scrollToSection }) => {
|
||||
const handleNavigation = (section) => {
|
||||
if (scrollToSection) {
|
||||
scrollToSection(section);
|
||||
} else {
|
||||
window.location.href = `/#${section}`;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="bg-yellow-500 text-white p-4 flex justify-between">
|
||||
<h1 className="text-3xl font-bold"></h1>
|
||||
<nav>
|
||||
<ul className="flex justify-end">
|
||||
<li className="mr-4">
|
||||
<a href="#" className="text-white hover:text-gray-300">
|
||||
<button
|
||||
onClick={() => handleNavigation("home")}
|
||||
className="text-white hover:text-gray-300"
|
||||
>
|
||||
Beranda
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
<li className="mr-4">
|
||||
<a href="#" className="text-white hover:text-gray-300">
|
||||
<button
|
||||
onClick={() => handleNavigation("tentang")}
|
||||
className="text-white hover:text-gray-300"
|
||||
>
|
||||
Tentang
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="text-white hover:text-gray-300">
|
||||
<button
|
||||
onClick={() => handleNavigation("kontak")}
|
||||
className="text-white hover:text-gray-300"
|
||||
>
|
||||
Kontak
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
|
@ -26,22 +26,22 @@ const KategoriRating = () => {
|
|||
<div className="absolute inset-0 bg-yellow-500 opacity-85"></div>
|
||||
|
||||
{/* Konten utama */}
|
||||
<div className="relative flex w-full h-full items-center justify-between p-10">
|
||||
<div className="relative flex w-full h-full items-center justify-center p-10">
|
||||
{/* Teks di sebelah kiri */}
|
||||
<div className="w-1/2 pr-5">
|
||||
<p className="text-black text-bold text-3xl">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eu turpis
|
||||
molestie, dictum est a, mattis tellus. Sed dignissim, metus nec fringilla
|
||||
accumsan, risus sem sollicitudin lacus, ut interdum tellus elit sed risus.
|
||||
Maecenas eget condimentum velit, sit amet feugiat lectusLorem ipsum dolor
|
||||
sit amet, consectetur adipiscing elit. Etiam eu turpismolestie, dictum est
|
||||
a, mattis tellus. Sed dignissim, metus nec fringillaaccumsan, risus sem
|
||||
sollicitudin lacus,ut interdum tellus elit sed risus.amet feugiat
|
||||
<p className="text-black text-bold text-2xl">
|
||||
Dalam Peraturan Menteri Komunikasi dan Informatika Nomor 2 Tahun 2024,
|
||||
gim diklasifikasikan berdasarkan kelompok usia pengguna: 3+, 7+, 13+, dan 18+.
|
||||
Setiap kategori ini mempertimbangkan jenis konten yang ditampilkan, seperti kekerasan,
|
||||
bahasa, unsur horor, hingga interaksi daring. Semakin tinggi kelompok usia,
|
||||
semakin kompleks pula konten yang diizinkan, namun tetap dalam batasan yang
|
||||
tidak melanggar norma dan hukum yang berlaku. Klasifikasi ini bertujuan agar
|
||||
setiap pengguna dapat memainkan gim yang sesuai dengan tingkat kedewasaan mereka,
|
||||
serta membantu orang tua dalam mengawasi pilihan hiburan digital anak-anaknya.
|
||||
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Kotak putih sejajar kiri-kanan dengan efek flip */}
|
||||
<div className="w-1/2 flex flex-col items-end gap-5 relative">
|
||||
<div className="flex gap-5 overflow-hidden w-[500px]">
|
||||
{cards.slice(currentIndex, currentIndex + 2).map((card, index) => (
|
||||
|
|
|
@ -4,7 +4,10 @@ const Tentang = () => {
|
|||
return (
|
||||
<div className="bg-[#BBBBBB] p-5 text-black text-center w-full h-[230px]">
|
||||
<h1 className="text-3xl font-bold mt-4">Tentang</h1>
|
||||
<h1 className="text-2xl mb-4">Worem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vulputate libero et velit interdum, ac aliquet odio mattis. Class aptent taciti sociosqu</h1>
|
||||
<h1 className="text-xl mb-4">Klasifikasi gim itu penting, bukan untuk membatasi, tetapi agar semua orang terutama anak muda dapat bermain dengan aman dan sesuai usia.
|
||||
Berdasarkan Peraturan Menteri Komunikasi dan Informatika Nomor 2 Tahun 2024, setiap gim yang beredar di Indonesia perlu melewati proses klasifikasi yang
|
||||
mempertimbangkan konten dan nilai-nilai budaya kita. Di sini, kami mendukung langkah ini dengan semangat terbuka, agar para pengembang dan pemain sama-sama
|
||||
tahu mana yang pas buat siapa. Soal seru-seruan, tetap jalan, tapi tetap bijak juga.</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import AppRoutes from "./routes/routes";
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<AppRoutes />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import Header from "../components/Header";
|
||||
import Footer from "../components/Footer";
|
||||
|
||||
const ListGames = () => {
|
||||
const [games, setGames] = useState([]);
|
||||
const [filteredGames, setFilteredGames] = useState([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedRatings, setSelectedRatings] = useState([]);
|
||||
|
||||
// Daftar rating dengan deskripsi dan warna
|
||||
const ratingDescriptions = {
|
||||
"SU": { label: "Semua Umur", description: "Dapat dimainkan oleh semua orang tanpa batasan konten.", color: "text-green-600" },
|
||||
"3+": { label: "Usia 3+", description: "Tidak ada konten dewasa, perjudian, atau interaksi online.", color: "text-blue-600" },
|
||||
"7+": { label: "Usia 7+", description: "Ada unsur ringan seperti kekerasan kartun atau bahasa ringan.", color: "text-yellow-600" },
|
||||
"13+": { label: "Usia 13+", description: "Ada kekerasan moderat, simulasi perjudian, atau tema horor.", color: "text-orange-600" },
|
||||
"18+": { label: "Usia 18+", description: "Mengandung konten dewasa, kekerasan realistis, atau humor kasar.", color: "text-red-600" }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch("http://127.0.0.1:5000/all_games")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const formattedData = data.map(game => ({
|
||||
name: game["Nama Game"],
|
||||
rating: game["Rating"]
|
||||
}));
|
||||
|
||||
setGames(formattedData);
|
||||
setFilteredGames(formattedData);
|
||||
})
|
||||
.catch(err => console.error("Error fetching game list:", err));
|
||||
}, []);
|
||||
|
||||
|
||||
// 🔍 Filter berdasarkan rating
|
||||
const handleRatingFilter = (rating) => {
|
||||
let updatedFilters = [...selectedRatings];
|
||||
|
||||
if (updatedFilters.includes(rating)) {
|
||||
updatedFilters = updatedFilters.filter((r) => r !== rating);
|
||||
} else {
|
||||
updatedFilters.push(rating);
|
||||
}
|
||||
|
||||
setSelectedRatings(updatedFilters);
|
||||
|
||||
if (updatedFilters.length === 0 || updatedFilters.includes("SU")) {
|
||||
setFilteredGames(games);
|
||||
} else {
|
||||
const filtered = games.filter((game) => updatedFilters.includes(game.rating));
|
||||
setFilteredGames(filtered);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 🔎 Search game berdasarkan nama
|
||||
const handleSearch = (event) => {
|
||||
const term = event.target.value.toLowerCase();
|
||||
setSearchTerm(term);
|
||||
|
||||
const filtered = games.filter(game =>
|
||||
game.name.toLowerCase().includes(term)
|
||||
);
|
||||
|
||||
setFilteredGames(filtered);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
|
||||
<div className="p-10">
|
||||
<h1 className="text-3xl font-bold mb-6">Daftar Game</h1>
|
||||
|
||||
{/* Grid untuk filter di kiri, search + list game di kanan */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{/* Bagian 1: Filter Rating */}
|
||||
<aside className="bg-gray-200 p-4 rounded h-fit">
|
||||
<h2 className="text-xl font-bold mb-4">Filter Rating</h2>
|
||||
<div className="space-y-2">
|
||||
{Object.keys(ratingDescriptions).map((rating, index) => (
|
||||
<label key={index} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-4 h-4"
|
||||
checked={selectedRatings.includes(rating)}
|
||||
onChange={() => handleRatingFilter(rating)}
|
||||
/>
|
||||
<span>{ratingDescriptions[rating].label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Bagian 2: Daftar Game */}
|
||||
<main className="col-span-3">
|
||||
{/* Bagian 3: Search Form */}
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari game..."
|
||||
className="p-2 border w-full"
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Card List Game */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{filteredGames.length > 0 ? (
|
||||
filteredGames.map((game, index) => {
|
||||
const ratingInfo = ratingDescriptions[game.rating] || {};
|
||||
return (
|
||||
<div key={index} className="border p-4 bg-gray-200 rounded shadow-md">
|
||||
<h3 className="text-lg font-bold">{game.name}</h3>
|
||||
<p className={`font-semibold ${ratingInfo.color}`}>
|
||||
Rating: {ratingInfo.label}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{ratingInfo.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-gray-500">Tidak ada game yang sesuai.</p>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer className="mt-auto" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListGames;
|
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
import MainComponent from "../App";
|
||||
import ListGames from "../pages/ListGame";
|
||||
|
||||
const AppRoutes = () => {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<MainComponent />} />
|
||||
<Route path="/list-games" element={<ListGames />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppRoutes;
|
|
@ -2,7 +2,18 @@
|
|||
export default {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx,css}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
'primary': '#FF0000',
|
||||
'secondary': '#00FF00',
|
||||
'tertiary': '#0000FF',
|
||||
'quaternary': '#FFFF00',
|
||||
'quinary': '#FF00FF',
|
||||
'darkpurple' : '#494287',
|
||||
'yellowlight' : '#ffcc23'
|
||||
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,136 @@
|
|||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.edge.service import Service
|
||||
from selenium.webdriver.edge.options import Options
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
import time
|
||||
|
||||
EDGE_DRIVER_PATH = "./msedgedriver.exe"
|
||||
|
||||
options = Options()
|
||||
service = Service(EDGE_DRIVER_PATH)
|
||||
driver = webdriver.Edge(service=service, options=options)
|
||||
wait = WebDriverWait(driver, 10)
|
||||
|
||||
def jawab_pertanyaan():
|
||||
for i in range(5):
|
||||
print(f"🔹 Menjawab pertanyaan ke-{i + 1}")
|
||||
option = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Agak Mengandung')]")))
|
||||
option.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
confirm = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Lanjut')]")))
|
||||
confirm.click()
|
||||
time.sleep(1)
|
||||
|
||||
def konfirmasi_hasil():
|
||||
print("🔹 Klik tombol Cek Hasil")
|
||||
cek_hasil_btn = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Cek Hasil')]")))
|
||||
cek_hasil_btn.click()
|
||||
time.sleep(1)
|
||||
|
||||
print("🔹 Konfirmasi pop-up Cek Hasil")
|
||||
confirm_final = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Lanjut')]")))
|
||||
confirm_final.click()
|
||||
|
||||
print("🔹 Klik tombol Coba Lagi")
|
||||
retry_btn = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Coba Lagi')]")))
|
||||
driver.save_screenshot(f"hasil_selenium_{int(time.time())}.png")
|
||||
retry_btn.click()
|
||||
time.sleep(1)
|
||||
|
||||
try:
|
||||
print("🔹 Membuka halaman...")
|
||||
driver.get("http://localhost:3000")
|
||||
time.sleep(2)
|
||||
|
||||
# Tes pertama: input game manual
|
||||
print("🔹 Mengisi nama game")
|
||||
input_field = wait.until(EC.presence_of_element_located((By.TAG_NAME, "input")))
|
||||
input_field.send_keys("Game Selenium Test 2025")
|
||||
time.sleep(1)
|
||||
|
||||
print("🔹 Klik tombol Cek")
|
||||
check_button = driver.find_element(By.XPATH, "//button[text()='Cek']")
|
||||
check_button.click()
|
||||
time.sleep(1)
|
||||
|
||||
jawab_pertanyaan()
|
||||
konfirmasi_hasil()
|
||||
|
||||
# Klik Game Terbaru
|
||||
# print("🔁 Uji klik Game Terbaru satu per satu")
|
||||
# container = wait.until(EC.presence_of_element_located((By.XPATH, "//h2[contains(text(),'Game Terbaru')]/..")))
|
||||
# game_elements = container.find_elements(By.TAG_NAME, "button")
|
||||
# print(f"🔍 Ditemukan {len(game_elements)} game terbaru")
|
||||
|
||||
# for idx, game in enumerate(game_elements[:4]):
|
||||
# print(f"🔹 Klik game terbaru ke-{idx + 1}: {game.text}")
|
||||
# driver.execute_script("arguments[0].scrollIntoView(true);", game)
|
||||
# game.click()
|
||||
# time.sleep(1)
|
||||
|
||||
# retry_btn = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Coba Lagi')]")))
|
||||
# driver.save_screenshot(f"klik_game_terbaru_{idx+1}.png")
|
||||
# retry_btn.click()
|
||||
# time.sleep(1)
|
||||
|
||||
# retry_btn = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Coba Lagi')]")))
|
||||
# driver.save_screenshot(f"klik_game_terbaru_{idx+1}.png")
|
||||
# retry_btn.click()
|
||||
# time.sleep(1)
|
||||
|
||||
# Klik Selengkapnya
|
||||
print("🔹 Klik tombol Selengkapnya")
|
||||
selengkapnya = wait.until(EC.element_to_be_clickable((By.LINK_TEXT, "Selengkapnya")))
|
||||
selengkapnya.click()
|
||||
time.sleep(2)
|
||||
|
||||
# Filterisasi: ceklist & unceklist semua filter
|
||||
print("Uji fitur filter rating")
|
||||
filter_labels = ["Semua Umur", "Usia 3+", "Usia 7+", "Usia 13+", "Usia 18+"]
|
||||
|
||||
for label in filter_labels:
|
||||
print(f"🔹 Klik filter: {label}")
|
||||
checkbox_label = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, f"//label[span[text()='{label}']]")
|
||||
))
|
||||
checkbox_label.click()
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
print(f"🔹 Nonaktifkan filter: {label}")
|
||||
checkbox_label.click()
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
# Kembali ke Beranda
|
||||
print("🔹 Klik tombol Beranda di header")
|
||||
beranda = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[text()='Beranda']")))
|
||||
beranda.click()
|
||||
time.sleep(2)
|
||||
|
||||
# Tes ulang input manual
|
||||
print("Tes ulang: Masukkan nama game yang sama")
|
||||
input_field = wait.until(EC.presence_of_element_located((By.TAG_NAME, "input")))
|
||||
input_field.send_keys("Game Selenium Test 2025")
|
||||
time.sleep(1)
|
||||
|
||||
print("🔹 Klik tombol Cek")
|
||||
check_button = driver.find_element(By.XPATH, "//button[text()='Cek']")
|
||||
check_button.click()
|
||||
time.sleep(1)
|
||||
|
||||
print("🔹 Klik tombol Coba Lagi")
|
||||
retry_btn = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Coba Lagi')]")))
|
||||
retry_btn.click()
|
||||
time.sleep(1)
|
||||
|
||||
print("✅ Selesai semua tahap!")
|
||||
|
||||
except Exception as e:
|
||||
print("❌ Terjadi kesalahan:", str(e))
|
||||
|
||||
finally:
|
||||
driver.quit()
|
Loading…
Reference in New Issue