menambahakn insert database dan testing selenium

This commit is contained in:
Zeakeers 2025-04-24 15:23:30 +07:00
parent 17b5eaa5b3
commit 90adfef859
15 changed files with 698 additions and 79 deletions

Binary file not shown.

View File

@ -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.

View File

@ -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": {

View File

@ -14,6 +14,7 @@
"description": "",
"dependencies": {
"react-dom": "^19.0.0",
"react-router-dom": "^7.2.0",
"react-scripts": "^5.0.1"
},
"browserslist": {

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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) => (

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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;

View File

@ -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;

View File

@ -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: [],
}

BIN
msedgedriver.exe Normal file

Binary file not shown.

136
test_game_rating_edge.py Normal file
View File

@ -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()