This commit is contained in:
Jevinca Marvella 2025-12-01 20:57:32 +07:00
commit cd0dccc729
11 changed files with 328 additions and 159 deletions

View File

@ -1047,14 +1047,14 @@ h1 {
}
}
/* Best Score Display - ORANGE gradient */
.best-score-display {
/* High Score Display - ORANGE gradient */
.high-score-display {
margin-top: 28px;
padding-top: 28px;
border-top: 2px solid rgba(255, 140, 0, 0.25);
}
.best-score-label {
.high-score-label {
font-size: 11px;
text-transform: uppercase;
color: rgba(255, 160, 50, 0.7);
@ -1063,7 +1063,7 @@ h1 {
font-weight: 700;
}
.best-score-value {
.high-score-value {
font-size: clamp(34px, 5vw, 42px);
font-weight: 900;
background: linear-gradient(135deg, #ff8c00 0%, #ffa500 50%, #ffb347 100%);

View File

@ -210,7 +210,7 @@
</div>
<div class="score-box">
<div class="score-label">HIGH SCORE</div>
<div class="score-value" id="best-score">0</div>
<div class="score-value" id="high-score">0</div>
</div>
</div>
</div>
@ -325,14 +325,14 @@
New High Score
</div>
<!-- Best Score Display - show if NOT new record -->
<!-- high Score Display - show if NOT new record -->
<div
class="best-score-display"
id="best-score-display"
class="high-score-display"
id="high-score-display"
style="display: none"
>
<div class="best-score-label">High Score</div>
<div class="best-score-value" id="modal-best-score">0</div>
<div class="high-score-label">High Score</div>
<div class="high-score-value" id="modal-high-score">0</div>
</div>
</div>

46
2048.js
View File

@ -1,11 +1,18 @@
/* ------------------------
State & Variables
------------------------ */
let board = [];
let currentScore = 0;
let bestScore = parseInt(localStorage.getItem('bestScore2048')) || 0;
// 1. Ambil username dari sessionStorage (sesuai sistem login kamu)
const currentUser = sessionStorage.getItem("loggedInUser") || "guest";
// 2. Buat nama kunci unik, misal: "highScore2048_budi"
const storageKey = 'highScore2048_' + currentUser;
// 3. Ambil skor milik user tersebut saja
let highScore = parseInt(localStorage.getItem(storageKey)) || 0;
let lastMoveDir = null;
let isMoving = false;
let mergesInCurrentMove = 0;
@ -71,7 +78,7 @@ function checkAndShowTutorial() {
DOM Ready
------------------------ */
document.addEventListener("DOMContentLoaded", () => {
updateBestScoreDisplay();
updateHighScoreDisplay();
setupBoard();
addNewTile();
addNewTile();
@ -223,17 +230,18 @@ function updateScoreDisplay() {
scoreEl.textContent = currentScore;
}
if (currentScore > bestScore) {
bestScore = currentScore;
localStorage.setItem('bestScore2048', bestScore);
updateBestScoreDisplay();
}
if (currentScore > highScore) {
highScore = currentScore;
// Gunakan storageKey yang sudah kita buat di atas (dinamis sesuai user)
localStorage.setItem(storageKey, highScore);
updateHighScoreDisplay();
}
}
function updateBestScoreDisplay() {
const bestScoreEl = document.getElementById('best-score');
if (bestScoreEl) {
bestScoreEl.textContent = bestScore;
function updateHighScoreDisplay() {
const highScoreEl = document.getElementById('high-score');
if (highScoreEl) {
highScoreEl.textContent = highScore;
}
}
@ -601,7 +609,7 @@ function showGameOver() {
console.error("Fungsi saveScore tidak ditemukan! Pastikan Score_Request.js sudah diload.");
}
const isNewHighScore = finalScore >= bestScore && finalScore > 0;
const isNewHighScore = finalScore >= highScore && finalScore > 0;
const finalScoreEl = document.getElementById('final-score');
if (finalScoreEl) {
@ -609,16 +617,16 @@ function showGameOver() {
}
const newHighScoreBadge = document.getElementById('new-high-score-badge');
const bestScoreDisplay = document.getElementById('best-score-display');
const highScoreDisplay = document.getElementById('high-score-display');
if (isNewHighScore) {
if (newHighScoreBadge) newHighScoreBadge.style.display = 'inline-block';
if (bestScoreDisplay) bestScoreDisplay.style.display = 'none';
if (highScoreDisplay) highScoreDisplay.style.display = 'none';
} else {
if (newHighScoreBadge) newHighScoreBadge.style.display = 'none';
if (bestScoreDisplay) bestScoreDisplay.style.display = 'block';
const modalBestScore = document.getElementById('modal-best-score');
if (modalBestScore) modalBestScore.textContent = bestScore;
if (highScoreDisplay) highScoreDisplay.style.display = 'block';
const modalHighScore = document.getElementById('modal-high-score');
if (modalHighScore) modalHighScore.textContent = highScore;
}
const gameOverOverlay = document.getElementById('game-over-overlay');

View File

@ -198,7 +198,9 @@ h1::before {
box-shadow: 0 0 15px rgba(0, 234, 255, 0.8);
}
/* Leaderboard Item */
/* --- LEADERBOARD ITEMS & COLORS (HARMONISASI) --- */
/* Base Item Style */
.leaderboard-item {
display: flex;
align-items: center;
@ -206,7 +208,7 @@ h1::before {
padding: 18px 25px;
border-radius: 14px;
background: rgba(30, 0, 50, 0.5);
border: 1px solid rgba(0, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
position: relative;
}
@ -217,57 +219,105 @@ h1::before {
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
}
/* Rank Styles */
.leaderboard-item.rank-1 {
background: linear-gradient(135deg, rgba(0, 234, 255, 0.25), rgba(0, 255, 136, 0.2));
border: 2px solid rgba(0, 234, 255, 0.6);
box-shadow:
0 0 25px rgba(0, 234, 255, 0.4),
inset 0 0 25px rgba(0, 234, 255, 0.15);
}
.leaderboard-item.rank-2 {
background: linear-gradient(135deg, rgba(255, 0, 255, 0.2), rgba(204, 0, 255, 0.15));
border: 2px solid rgba(255, 0, 255, 0.5);
box-shadow: 0 0 18px rgba(255, 0, 255, 0.3);
}
.leaderboard-item.rank-3 {
background: linear-gradient(135deg, rgba(138, 43, 226, 0.25), rgba(75, 0, 130, 0.2));
border: 2px solid rgba(138, 43, 226, 0.5);
box-shadow: 0 0 15px rgba(138, 43, 226, 0.3);
}
/* Rank Badge */
.rank-badge {
width: 45px;
height: 45px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
font-weight: bold;
flex-shrink: 0;
/* === RANK 1: CYAN NEON (Juara) === */
.leaderboard-item.rank-1,
.your-rank-container.rank-1 .your-rank-item {
background: linear-gradient(90deg, rgba(0, 234, 255, 0.25) 0%, rgba(0, 234, 255, 0.05) 100%);
border: 2px solid #00eaff;
box-shadow: 0 0 15px rgba(0, 234, 255, 0.3);
}
.rank-1 .rank-badge {
background: linear-gradient(135deg, #00eaff, #00ff88);
color: #0c001a;
box-shadow: 0 0 20px rgba(0, 234, 255, 0.8);
background: #00eaff;
color: #000;
box-shadow: 0 0 15px #00eaff;
animation: pulseBadge 2s ease-in-out infinite;
}
.rank-2 .rank-badge {
background: linear-gradient(135deg, #ff00ff, #cc00ff);
.rank-1 .player-name {
/* 1. Matikan efek gradient aneh pada teks */
background: none;
-webkit-text-fill-color: initial;
background-clip: border-box;
/* 2. Samakan ukuran font dengan yang lain (sebelumnya 22px jadi 20px) */
font-size: 20px;
/* 3. Warna Putih Solid + Sedikit Glow Cyan biar tetap spesial tapi rapi */
color: #ffffff;
box-shadow: 0 0 18px rgba(255, 0, 255, 0.7);
text-shadow: 0 0 10px rgba(0, 234, 255, 0.8);
/* Hapus filter drop-shadow berlebih */
filter: none;
}
/* FIX: Warna Score Rank 1 jadi Putih Solid (Anti Silau) */
.rank-1 .score-value {
/* 1. Ukuran disamakan dengan yang lain */
font-size: 22px !important;
/* 2. Warna Teks Putih Solid */
background: none !important;
-webkit-text-fill-color: initial !important;
color: #ffffff !important;
/* 3. GLOWING: Ganti bayangan hitam jadi cahaya Cyan */
text-shadow: 0 0 10px #00eaff, 0 0 20px rgba(0, 234, 255, 0.4) !important;
/* Reset filter */
filter: none !important;
}
/* === RANK 2: MAGENTA NEON (Runner Up) === */
.leaderboard-item.rank-2,
.your-rank-container.rank-2 .your-rank-item {
background: linear-gradient(90deg, rgba(255, 0, 255, 0.25) 0%, rgba(255, 0, 255, 0.05) 100%);
border: 2px solid #ff00ff;
box-shadow: 0 0 15px rgba(255, 0, 255, 0.3);
}
.rank-2 .rank-badge {
background: #ff00ff;
color: #fff;
box-shadow: 0 0 15px #ff00ff;
}
.rank-2 .score-value {
color: #fff;
text-shadow: 0 0 10px #ff00ff;
}
.rank-2 .player-score {
color: #ff00ff;
}
/* === RANK 3: VIOLET NEON (Third) === */
.leaderboard-item.rank-3,
.your-rank-container.rank-3 .your-rank-item {
background: linear-gradient(90deg, rgba(138, 43, 226, 0.25) 0%, rgba(138, 43, 226, 0.05) 100%);
border: 2px solid #8a2be2;
box-shadow: 0 0 15px rgba(138, 43, 226, 0.3);
}
.rank-3 .rank-badge {
background: linear-gradient(135deg, #8a2be2, #4b0082);
color: #ffffff;
box-shadow: 0 0 15px rgba(138, 43, 226, 0.6);
background: #8a2be2;
color: #fff;
box-shadow: 0 0 15px #8a2be2;
}
.rank-3 .score-value {
color: #fff;
text-shadow: 0 0 10px #8a2be2;
}
.rank-3 .player-score {
color: #a855f7;
}
/* === RANK OTHER === */
.leaderboard-item.rank-other {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.rank-other .rank-badge {
@ -276,6 +326,7 @@ h1::before {
border: 1px solid rgba(0, 255, 255, 0.3);
}
/* Animation */
@keyframes pulseBadge {
0%, 100% {
transform: scale(1);
@ -287,7 +338,7 @@ h1::before {
}
}
/* Player Info */
/* Player Info Common */
.player-info {
flex: 1;
display: flex;
@ -305,22 +356,11 @@ h1::before {
text-overflow: ellipsis;
}
.rank-1 .player-name {
background: linear-gradient(90deg, #00eaff, #00ff88);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: none;
filter: drop-shadow(0 0 6px rgba(0, 234, 255, 0.5));
font-size: 22px;
}
/* Score */
/* Score Common */
.player-score {
font-weight: 700;
font-size: 18px;
color: #00eaff;
text-shadow: 0 0 12px rgba(0, 234, 255, 0.7);
display: flex;
flex-direction: column;
align-items: flex-end;
@ -332,25 +372,6 @@ h1::before {
font-size: 22px;
}
.rank-1 .score-value {
font-size: 25px;
background: linear-gradient(90deg, #00eaff, #ff00ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
filter: drop-shadow(0 0 10px rgba(0, 234, 255, 0.8));
}
.rank-2 .player-score {
color: #ff00ff;
text-shadow: 0 0 12px rgba(255, 0, 255, 0.7);
}
.rank-3 .player-score {
color: #a855f7;
text-shadow: 0 0 12px rgba(138, 43, 226, 0.6);
}
.score-label {
font-size: 11px;
text-transform: uppercase;
@ -358,15 +379,26 @@ h1::before {
color: rgba(200, 200, 255, 0.5);
}
/* Your Rank Fixed Section */
/* Badge Size Common */
.rank-badge {
width: 45px;
height: 45px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
font-weight: bold;
flex-shrink: 0;
}
/* Your Rank Section Structure */
.your-rank-container {
background: rgba(20, 0, 40, 0.9);
border-radius: 14px;
padding: 15px 25px;
border: 2px solid rgba(0, 255, 136, 0.6);
box-shadow:
0 0 25px rgba(0, 255, 136, 0.4),
inset 0 0 20px rgba(0, 255, 136, 0.15);
background: transparent;
border: none;
box-shadow: none;
padding: 0;
margin-top: 20px;
}
.your-rank-item {
@ -375,15 +407,34 @@ h1::before {
gap: 20px;
padding: 18px 25px;
border-radius: 14px;
background: linear-gradient(135deg, rgba(0, 255, 136, 0.15), rgba(0, 234, 255, 0.1));
/* Default style if not top 3 */
background: linear-gradient(135deg, rgba(0, 255, 136, 0.15), rgba(0, 0, 0, 0.5));
border: 2px solid rgba(0, 255, 136, 0.5);
box-shadow: 0 0 20px rgba(0, 255, 136, 0.3);
}
/* Responsive */
/* --- RESPONSIVE FIX (MODE LAPTOP/LAYAR KECIL) --- */
@media screen and (max-width: 1440px) {
/* Mencegah container nabrak tombol back */
.container {
width: 88%;
padding: 30px 50px;
max-height: 90vh;
}
.btn-back {
top: 20px;
left: 20px;
width: 45px;
height: 45px;
}
}
@media (max-width: 768px) {
.container {
padding: 35px 30px;
width: 95%;
}
h1 {
@ -394,18 +445,6 @@ h1::before {
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.btn-back {
width: 45px;
height: 45px;
top: 20px;
left: 20px;
}
.btn-back svg {
width: 20px;
height: 20px;
}
}
@media (max-width: 480px) {

View File

@ -21,6 +21,8 @@
<ul class="leaderboard-list" id="leaderboardList"></ul>
<div id="userRankContainer"></div>
</div>
<script src="Leaderboard.js"></script>

View File

@ -7,12 +7,53 @@ function loadLeaderboard() {
.then(response => response.json())
.then(data => {
if (data.status === "success") {
// Render Top 10
renderLeaderboard(data.leaderboard);
// Render Ranking Saya (User Rank)
if (data.user_rank) {
renderUserRank(data.user_rank);
}
}
})
.catch(error => console.error("Error loading leaderboard:", error));
}
// ... fungsi renderLeaderboard tetap sama ...
// TAMBAHKAN FUNGSI INI DI BAWAH
function renderUserRank(user) {
const container = document.getElementById('userRankContainer');
if (!container) return;
// Format angka skor
const formattedScore = new Intl.NumberFormat().format(user.score);
// HTML structure sesuai CSS .your-rank-container
const html = `
<div class="your-rank-container">
<div style="color: #00ff88; margin-bottom: 10px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px;">
</div>
<div class="your-rank-item">
<div class="rank-badge" style="background: #00ff88; color: #000; box-shadow: 0 0 15px #00ff88;">
${user.rank}
</div>
<div class="player-info">
<div class="player-name">${escapeHtml(user.username)}</div>
</div>
<div class="player-score">
<div class="score-value">${formattedScore}</div>
<div class="score-label">Points</div>
</div>
</div>
</div>
`;
container.innerHTML = html;
}
// ... fungsi escapeHtml tetap sama ...
function renderLeaderboard(players) {
const listContainer = document.getElementById('leaderboardList');
if (!listContainer) return; // Safety check

View File

@ -1,23 +1,76 @@
<?php
header('Content-Type: application/json');
require 'Connection.php';
session_start();
$query = "SELECT username, score FROM leaderboard ORDER BY score DESC LIMIT 10";
$response = [
"status" => "error",
"leaderboard" => [],
"user_rank" => null
];
// ---------------------------------------------------------
// 1. Ambil Top 10 Global (DIPERBAIKI)
// Ditambahkan "user_id ASC" agar urutannya PASTI (Konsisten)
// Jika skor sama, user dengan ID lebih kecil (daftar duluan) akan di atas
// ---------------------------------------------------------
$query = "SELECT username, score FROM leaderboard ORDER BY score DESC, user_id ASC LIMIT 10";
$result = $conn->query($query);
$leaderboard = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$leaderboard[] = $row;
$response['leaderboard'][] = $row;
}
}
// Kirim data ke frontend dalam bentuk JSON
echo json_encode([
"status" => "success",
"leaderboard" => $leaderboard
]);
// ---------------------------------------------------------
// 2. Ambil Ranking User Login (DIPERBAIKI)
// ---------------------------------------------------------
if (isset($_SESSION['user_id'])) {
$my_id = $_SESSION['user_id'];
// Ambil score user saat ini
$scoreQuery = $conn->prepare("SELECT username, score FROM leaderboard WHERE user_id = ?");
$scoreQuery->bind_param("i", $my_id);
$scoreQuery->execute();
$scoreResult = $scoreQuery->get_result();
if ($scoreRow = $scoreResult->fetch_assoc()) {
$myScore = $scoreRow['score'];
$myUsername = $scoreRow['username'];
// --- LOGIKA BARU ---
// Hitung ranking dengan Tie-Breaker.
// Rank = Jumlah orang yang skornya LEBIH BESAR
// DITAMBAH Jumlah orang yang skornya SAMA tapi ID-nya LEBIH KECIL (dia di atas kita)
$rankQuery = $conn->prepare("
SELECT COUNT(*) as rank_above
FROM leaderboard
WHERE score > ?
OR (score = ? AND user_id < ?)
");
// Kita bind 3 parameter: score, score, id
$rankQuery->bind_param("iii", $myScore, $myScore, $my_id);
$rankQuery->execute();
$rankResult = $rankQuery->get_result();
$rankRow = $rankResult->fetch_assoc();
// Rank adalah jumlah orang di atas kita + 1
$myRank = $rankRow['rank_above'] + 1;
$response['user_rank'] = [
"username" => $myUsername,
"score" => $myScore,
"rank" => $myRank
];
}
}
$response['status'] = "success";
echo json_encode($response);
$conn->close();
?>

View File

@ -1,12 +1,11 @@
<?php
// ✅ CORS Headers HARUS di paling atas sebelum apapun
// ... (Header CORS tetap sama) ...
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 86400');
header('Content-Type: application/json');
// ✅ Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
@ -15,17 +14,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
session_start();
include 'Connection.php';
// Ambil data dari JSON body
$input = json_decode(file_get_contents('php://input'), true);
$username = $input['username'] ?? '';
$password = $input['password'] ?? '';
if (empty($username) || empty($password)) {
echo json_encode(["success" => false, "message" => "Username dan password wajib diisi"]);
exit;
}
// ... (Validasi input kosong tetap sama) ...
$stmt = $conn->prepare("SELECT password FROM users WHERE username = ?");
// 🔴 PERBAIKAN 1: Tambahkan 'id' di dalam SELECT
$stmt = $conn->prepare("SELECT id, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
@ -37,11 +33,15 @@ if ($stmt->num_rows === 0) {
exit;
}
$stmt->bind_result($hashedPassword);
// 🔴 PERBAIKAN 2: Bind result untuk menangkap 'id' dan 'password'
$stmt->bind_result($userId, $hashedPassword);
$stmt->fetch();
if (password_verify($password, $hashedPassword)) {
// 🔴 PERBAIKAN 3: Simpan 'user_id' ke dalam SESSION
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $username;
echo json_encode([
"success" => true,
"message" => "Login berhasil",

View File

@ -62,6 +62,7 @@
</div>
</div>
<script src="Score_Request.js"></script>
<script type="module" src="Register.js"></script>
<script src="Animation_Register.js"></script>
</body>

View File

@ -28,17 +28,34 @@ document.getElementById("registerForm").addEventListener("submit", async functio
// Button loading state
const submitBtn = this.querySelector("button[type='submit']");
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
try {
const data = await registerRequest(username, password);
// API temenmu return: { status: "success", message: "..." }
if (data.status === "success") {
// 1. Tampilkan modal sukses
showModal("success", "Register Successful!", data.message);
// 🔥 PERBAIKAN: SAVE SCORE & REDIRECT 🔥
// Coba simpan skor jika variable 'score' atau 'gameScore' tersedia di global scope
// Atau jika kamu menyimpan skor sementara di localStorage
const pendingScore = localStorage.getItem('lastScore'); // Contoh jika pakai localStorage
if (pendingScore && typeof saveScore === "function") {
console.log("Menyimpan skor pending: " + pendingScore);
saveScore(pendingScore);
} else if (typeof score !== 'undefined') {
// Jika variabel global 'score' ada (dari file game logic)
saveScore(score);
}
// Redirect ke halaman utama setelah 1.5 detik
setTimeout(() => {
window.location.href = "index.html"; // Ubah sesuai halaman game/menu kamu
}, 1500);
} else {
showModal("error", "Register Failed!", data.message || "An error occurred.");
setInputError("username");
@ -48,11 +65,9 @@ document.getElementById("registerForm").addEventListener("submit", async functio
console.error("Register Error:", error);
let msg = "A connection error occurred.";
if (error.message === "Failed to fetch") {
msg = "Unable to connect to server.";
}
showModal("error", "Error!", msg);
} finally {
@ -68,8 +83,10 @@ document.getElementById("confirmPassword").addEventListener("input", clearInputS
// Efek error merah neon
function setInputError(id) {
const el = document.getElementById(id);
if(el) {
el.style.borderColor = "#ff0080";
el.style.boxShadow = "0 0 20px rgba(255, 0, 128, 0.3)";
}
}
// Hilangkan error visual

View File

@ -2,12 +2,12 @@
// ✅ Set timezone Indonesia (WIB)
date_default_timezone_set('Asia/Jakarta');
// ✅ CORS Headers - di paling atas
// ✅ CORS Headers
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 86400');
header('Content-Type: application/json'); // Cukup 1x saja
header('Content-Type: application/json');
// ✅ Handle preflight OPTIONS
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
@ -17,7 +17,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
include 'Connection.php';
// ✅ Handle input dari JSON body atau POST form
// ✅ Handle input
$input = json_decode(file_get_contents('php://input'), true);
$username = trim($input['username'] ?? $_POST['username'] ?? '');
$password = $input['password'] ?? $_POST['password'] ?? '';
@ -31,7 +31,7 @@ if (empty($username) || empty($password)) {
exit;
}
// ✅ Validasi panjang password minimal
// ✅ Validasi panjang password
if (strlen($password) < 6) {
echo json_encode([
"status" => "error",
@ -68,15 +68,23 @@ $check->close();
// ✅ Hash password dan insert ke database
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$created_at = date("Y-m-d H:i:s");
$stmt = $conn->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->bind_param("ss", $username, $hashedPassword);
$stmt = $conn->prepare("INSERT INTO users (username, password, created_at) VALUES (?, ?, ?)");
$stmt->bind_param("sss", $username, $hashedPassword, $created_at);
if ($stmt->execute()) {
// 🔥 PERBAIKAN UTAMA DI SINI (AUTO-LOGIN) 🔥
$new_user_id = $stmt->insert_id; // Ambil ID user baru
session_start();
$_SESSION['user_id'] = $new_user_id; // Set Session ID
$_SESSION['username'] = $username; // Set Session Username
echo json_encode([
"status" => "success",
"message" => "Pendaftaran berhasil",
"registered_at" => date("Y-m-d H:i:s") // timestamp sudah WIB
"message" => "Pendaftaran berhasil & Auto-login",
"registered_at" => $created_at
]);
} else {
echo json_encode([