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 flask import Flask, request, jsonify
|
||||||
|
from sklearn.metrics import confusion_matrix, classification_report, accuracy_score
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from sklearn.neighbors import KNeighborsClassifier
|
from sklearn.neighbors import KNeighborsClassifier
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import os
|
import os
|
||||||
|
import mysql.connector
|
||||||
|
|
||||||
|
# Konfigurasi koneksi database MySQL
|
||||||
|
DB_CONFIG = {
|
||||||
|
'host': 'localhost',
|
||||||
|
'user': 'root',
|
||||||
|
'password': '',
|
||||||
|
'database': 'game_rating_db'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
@ -60,6 +71,42 @@ def add_to_dataset(name, features, rating):
|
||||||
data.loc[len(data)] = new_row
|
data.loc[len(data)] = new_row
|
||||||
data.to_excel(DATASET_FILE, index=False)
|
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
|
# Endpoint untuk mengecek apakah game sudah ada dalam dataset
|
||||||
@app.route('/check_game', methods=['POST'])
|
@app.route('/check_game', methods=['POST'])
|
||||||
def check_game():
|
def check_game():
|
||||||
|
@ -89,21 +136,21 @@ def submit_answers():
|
||||||
answers = data.get('answers', [])
|
answers = data.get('answers', [])
|
||||||
|
|
||||||
if not game_name or not 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
|
return jsonify({"error": "Data tidak lengkap"}), 400
|
||||||
|
|
||||||
print("✔ Nama Game:", game_name)
|
print("Nama Game:", game_name)
|
||||||
print("✔ Jawaban:", answers)
|
print("Jawaban:", answers)
|
||||||
|
|
||||||
# Cek apakah jawaban valid
|
# Cek apakah jawaban valid
|
||||||
features = []
|
features = []
|
||||||
for answer in answers:
|
for answer in answers:
|
||||||
if answer not in REVERSE_MAPPING:
|
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
|
return jsonify({"error": f"Jawaban tidak valid: {answer}"}), 400
|
||||||
features.append(REVERSE_MAPPING[answer])
|
features.append(REVERSE_MAPPING[answer])
|
||||||
|
|
||||||
print("✔ Features (dikonversi ke angka):", features)
|
print("Features (dikonversi ke angka):", features)
|
||||||
|
|
||||||
|
|
||||||
X, y, dataset = load_dataset()
|
X, y, dataset = load_dataset()
|
||||||
|
@ -111,12 +158,12 @@ def submit_answers():
|
||||||
# Jika game sudah ada di dataset, kembalikan rating yang sudah ada
|
# Jika game sudah ada di dataset, kembalikan rating yang sudah ada
|
||||||
if game_name in dataset["Nama Game"].values:
|
if game_name in dataset["Nama Game"].values:
|
||||||
existing_rating = dataset.loc[dataset["Nama Game"] == game_name, "Rating"].values[0]
|
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})
|
return jsonify({'rating': existing_rating})
|
||||||
|
|
||||||
# Prediksi rating dengan KNN jika dataset cukup besar
|
# Prediksi rating dengan KNN jika dataset cukup besar
|
||||||
if len(X) > 3:
|
if len(X) > 3:
|
||||||
knn = KNeighborsClassifier(n_neighbors=3)
|
knn = KNeighborsClassifier(n_neighbors=5)
|
||||||
knn.fit(X, y)
|
knn.fit(X, y)
|
||||||
predicted_rating = knn.predict([features])[0]
|
predicted_rating = knn.predict([features])[0]
|
||||||
else:
|
else:
|
||||||
|
@ -132,14 +179,112 @@ def submit_answers():
|
||||||
else:
|
else:
|
||||||
predicted_rating = "18+"
|
predicted_rating = "18+"
|
||||||
|
|
||||||
print("✔ Prediksi Rating:", predicted_rating)
|
print("Prediksi Rating:", predicted_rating)
|
||||||
|
|
||||||
add_to_dataset(game_name, features, predicted_rating)
|
add_to_dataset(game_name, features, predicted_rating)
|
||||||
|
save_rating_to_db(game_name, predicted_rating)
|
||||||
return jsonify({'rating': predicted_rating})
|
return jsonify({'rating': predicted_rating})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("❌ ERROR di backend:", str(e))
|
print("ERROR di backend:", str(e))
|
||||||
return jsonify({"error": str(e)}), 500
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -10,6 +10,7 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.2.0",
|
||||||
"react-scripts": "^5.0.1"
|
"react-scripts": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -3542,6 +3543,12 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "8.56.12",
|
"version": "8.56.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
|
||||||
|
@ -13752,6 +13759,55 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-scripts": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||||
|
@ -14654,6 +14710,12 @@
|
||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
@ -16020,6 +16082,12 @@
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||||
"license": "0BSD"
|
"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": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
@ -16150,9 +16218,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.7.3",
|
"version": "4.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -16160,7 +16228,7 @@
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.17"
|
"node": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.2.0",
|
||||||
"react-scripts": "^5.0.1"
|
"react-scripts": "^5.0.1"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"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 Header from "./components/Header";
|
||||||
import Footer from "./components/Footer";
|
import Footer from "./components/Footer";
|
||||||
import Tentang from "./components/Tentang";
|
import Tentang from "./components/Tentang";
|
||||||
|
@ -39,6 +40,12 @@ const App = () => {
|
||||||
const [isPopUpOpen, setIsPopUpOpen] = useState(false);
|
const [isPopUpOpen, setIsPopUpOpen] = useState(false);
|
||||||
const [isConfirmPopupOpen, setIsConfirmPopupOpen] = useState(false);
|
const [isConfirmPopupOpen, setIsConfirmPopupOpen] = useState(false);
|
||||||
const [selectedOption, setSelectedOption] = useState("");
|
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(() => {
|
useEffect(() => {
|
||||||
if (gameName.length > 1) {
|
if (gameName.length > 1) {
|
||||||
|
@ -51,26 +58,46 @@ const App = () => {
|
||||||
}
|
}
|
||||||
}, [gameName]);
|
}, [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) => {
|
const handleSelectGame = (selectedGame) => {
|
||||||
setGameName(selectedGame);
|
setGameName(selectedGame);
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkGame = async () => {
|
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", {
|
const response = await fetch("http://127.0.0.1:5000/check_game", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ game_name: gameName }),
|
body: JSON.stringify({ game_name: gameName }),
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.exists) {
|
if (data.exists) {
|
||||||
setRating(data.rating);
|
setRating(data.rating);
|
||||||
} else {
|
} else {
|
||||||
setStep(1);
|
setStep(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOptionClick = (option) => {
|
const handleOptionClick = (option) => {
|
||||||
setSelectedOption(option);
|
setSelectedOption(option);
|
||||||
setIsPopUpOpen(true);
|
setIsPopUpOpen(true);
|
||||||
|
@ -97,10 +124,34 @@ const App = () => {
|
||||||
setRating(data.rating);
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header/>
|
<Header scrollToSection={scrollToSection} />
|
||||||
<div className="h-screen w-full bg-cover bg-center bg-no-repeat bg-black bg-opacity-50 flex items-center justify-center"
|
<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%" }}>
|
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' : ''}`}>
|
<div className={`w-full max-w-lg p-6 text-center relative ${step > 0 || rating ? 'bg-white rounded-lg shadow-md' : ''}`}>
|
||||||
{step === 0 && !rating && (
|
{step === 0 && !rating && (
|
||||||
|
@ -117,8 +168,7 @@ const App = () => {
|
||||||
className="w-6 h-6 bg-yellow-500 rounded-full hover:bg-yellow-700"
|
className="w-6 h-6 bg-yellow-500 rounded-full hover:bg-yellow-700"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tombol merah: Kembali ke input nama game */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStep(0);
|
setStep(0);
|
||||||
|
@ -129,7 +179,6 @@ const App = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!rating && step === 0 && (
|
{!rating && step === 0 && (
|
||||||
<>
|
<>
|
||||||
<p className="mt-4 text-left text-white">Masukkan nama game:</p>
|
<p className="mt-4 text-left text-white">Masukkan nama game:</p>
|
||||||
|
@ -158,9 +207,30 @@ const App = () => {
|
||||||
>
|
>
|
||||||
Cek
|
Cek
|
||||||
</button>
|
</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 && (
|
{!rating && step > 0 && step <= questions.length && (
|
||||||
<>
|
<>
|
||||||
<p className="mt-4">{questions[step - 1]}</p>
|
<p className="mt-4">{questions[step - 1]}</p>
|
||||||
|
@ -175,7 +245,6 @@ const App = () => {
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!rating && step > questions.length && (
|
{!rating && step > questions.length && (
|
||||||
<>
|
<>
|
||||||
{/* Menampilkan tabel hasil jawaban */}
|
{/* Menampilkan tabel hasil jawaban */}
|
||||||
|
@ -198,7 +267,6 @@ const App = () => {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tombol kembali ke input game */}
|
{/* Tombol kembali ke input game */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -210,7 +278,6 @@ const App = () => {
|
||||||
>
|
>
|
||||||
Kembali
|
Kembali
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Tombol untuk konfirmasi hasil */}
|
{/* Tombol untuk konfirmasi hasil */}
|
||||||
<button
|
<button
|
||||||
onClick={submitAnswers}
|
onClick={submitAnswers}
|
||||||
|
@ -222,26 +289,38 @@ const App = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{rating && (
|
{rating && (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-xl font-semibold mt-4">
|
{easterEggActive ? (
|
||||||
Game <span className="font-bold text-blue-500">{gameName}</span> memiliki rating
|
<h2 className="text-xl font-bold text-center text-red-600 mt-4">
|
||||||
<span className="font-bold text-red-500"> {rating}</span>
|
🎉 BISMILLAH LULUS 2025 DENGAN SEDIKIT REVISI 🎓
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2"><strong>Penjelasan:</strong> {ratingDescriptions[rating]}</p>
|
) : (
|
||||||
<button
|
<>
|
||||||
onClick={() => {
|
<h2 className="text-xl font-semibold mt-4">
|
||||||
setStep(0);
|
Game <span className="font-bold text-blue-500">{gameName}</span> memiliki rating
|
||||||
setAnswers([]);
|
<span className="font-bold text-red-500"> {rating}</span>
|
||||||
setRating(null);
|
</h2>
|
||||||
setGameName("");
|
<p className="mt-2"><strong>Penjelasan:</strong> {ratingDescriptions[rating]}</p>
|
||||||
}}
|
</>
|
||||||
className="bg-red-500 text-white px-4 py-2 rounded mt-4 w-full"
|
)}
|
||||||
>
|
|
||||||
Coba Lagi
|
<button
|
||||||
</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"
|
||||||
|
>
|
||||||
|
Coba Lagi
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Pop-up konfirmasi pilihan jawaban */}
|
{/* Pop-up konfirmasi pilihan jawaban */}
|
||||||
<PopUp
|
<PopUp
|
||||||
isOpen={isPopUpOpen}
|
isOpen={isPopUpOpen}
|
||||||
|
@ -259,9 +338,13 @@ const App = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tentang" ref={tentangRef}>
|
||||||
<Tentang />
|
<Tentang />
|
||||||
|
</div>
|
||||||
<KategoriRating />
|
<KategoriRating />
|
||||||
<Footer />
|
<div id="kontak" ref={kontakRef}>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +1,44 @@
|
||||||
import react from "react";
|
import react from "react";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = ({ scrollToSection }) => {
|
||||||
|
const handleNavigation = (section) => {
|
||||||
|
if (scrollToSection) {
|
||||||
|
scrollToSection(section);
|
||||||
|
} else {
|
||||||
|
window.location.href = `/#${section}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="bg-yellow-500 text-white p-4 flex justify-between">
|
<div className="bg-yellow-500 text-white p-4 flex justify-between">
|
||||||
<h1 className="text-3xl font-bold"></h1>
|
<h1 className="text-3xl font-bold"></h1>
|
||||||
<nav>
|
<nav>
|
||||||
<ul className="flex justify-end">
|
<ul className="flex justify-end">
|
||||||
<li className="mr-4">
|
<li className="mr-4">
|
||||||
<a href="#" className="text-white hover:text-gray-300">
|
<button
|
||||||
Beranda
|
onClick={() => handleNavigation("home")}
|
||||||
</a>
|
className="text-white hover:text-gray-300"
|
||||||
</li>
|
>
|
||||||
<li className="mr-4">
|
Beranda
|
||||||
<a href="#" className="text-white hover:text-gray-300">
|
</button>
|
||||||
Tentang
|
</li>
|
||||||
</a>
|
<li className="mr-4">
|
||||||
</li>
|
<button
|
||||||
<li>
|
onClick={() => handleNavigation("tentang")}
|
||||||
<a href="#" className="text-white hover:text-gray-300">
|
className="text-white hover:text-gray-300"
|
||||||
Kontak
|
>
|
||||||
</a>
|
Tentang
|
||||||
</li>
|
</button>
|
||||||
</ul>
|
</li>
|
||||||
</nav>
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => handleNavigation("kontak")}
|
||||||
|
className="text-white hover:text-gray-300"
|
||||||
|
>
|
||||||
|
Kontak
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,22 +26,22 @@ const KategoriRating = () => {
|
||||||
<div className="absolute inset-0 bg-yellow-500 opacity-85"></div>
|
<div className="absolute inset-0 bg-yellow-500 opacity-85"></div>
|
||||||
|
|
||||||
{/* Konten utama */}
|
{/* 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 */}
|
{/* Teks di sebelah kiri */}
|
||||||
<div className="w-1/2 pr-5">
|
<div className="w-1/2 pr-5">
|
||||||
<p className="text-black text-bold text-3xl">
|
<p className="text-black text-bold text-2xl">
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eu turpis
|
Dalam Peraturan Menteri Komunikasi dan Informatika Nomor 2 Tahun 2024,
|
||||||
molestie, dictum est a, mattis tellus. Sed dignissim, metus nec fringilla
|
gim diklasifikasikan berdasarkan kelompok usia pengguna: 3+, 7+, 13+, dan 18+.
|
||||||
accumsan, risus sem sollicitudin lacus, ut interdum tellus elit sed risus.
|
Setiap kategori ini mempertimbangkan jenis konten yang ditampilkan, seperti kekerasan,
|
||||||
Maecenas eget condimentum velit, sit amet feugiat lectusLorem ipsum dolor
|
bahasa, unsur horor, hingga interaksi daring. Semakin tinggi kelompok usia,
|
||||||
sit amet, consectetur adipiscing elit. Etiam eu turpismolestie, dictum est
|
semakin kompleks pula konten yang diizinkan, namun tetap dalam batasan yang
|
||||||
a, mattis tellus. Sed dignissim, metus nec fringillaaccumsan, risus sem
|
tidak melanggar norma dan hukum yang berlaku. Klasifikasi ini bertujuan agar
|
||||||
sollicitudin lacus,ut interdum tellus elit sed risus.amet feugiat
|
setiap pengguna dapat memainkan gim yang sesuai dengan tingkat kedewasaan mereka,
|
||||||
|
serta membantu orang tua dalam mengawasi pilihan hiburan digital anak-anaknya.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Kotak putih sejajar kiri-kanan dengan efek flip */}
|
|
||||||
<div className="w-1/2 flex flex-col items-end gap-5 relative">
|
<div className="w-1/2 flex flex-col items-end gap-5 relative">
|
||||||
<div className="flex gap-5 overflow-hidden w-[500px]">
|
<div className="flex gap-5 overflow-hidden w-[500px]">
|
||||||
{cards.slice(currentIndex, currentIndex + 2).map((card, index) => (
|
{cards.slice(currentIndex, currentIndex + 2).map((card, index) => (
|
||||||
|
|
|
@ -4,7 +4,10 @@ const Tentang = () => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#BBBBBB] p-5 text-black text-center w-full h-[230px]">
|
<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-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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import AppRoutes from "./routes/routes";
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<AppRoutes />
|
||||||
</React.StrictMode>
|
</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 {
|
export default {
|
||||||
content: ["./src/**/*.{js,jsx,ts,tsx,css}"],
|
content: ["./src/**/*.{js,jsx,ts,tsx,css}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
colors: {
|
||||||
|
'primary': '#FF0000',
|
||||||
|
'secondary': '#00FF00',
|
||||||
|
'tertiary': '#0000FF',
|
||||||
|
'quaternary': '#FFFF00',
|
||||||
|
'quinary': '#FF00FF',
|
||||||
|
'darkpurple' : '#494287',
|
||||||
|
'yellowlight' : '#ffcc23'
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
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