website
complite html css js complite coding
spin ern web or app complete
imgs
complite html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SpinEarn - Spin & Earn by Lakhhna</title>
<!-- Google Font: Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
/* CSS Reset and Base Styles */
:root {
--primary-color: #3b82f6; /* Blue */
--primary-color-dark: #2563eb;
--primary-color-light: #60a5fa;
--secondary-color: #10b981; /* Green */
--danger-color: #ef4444; /* Red */
--warning-color: #f59e0b; /* Yellow/Orange */
--dark-color: #1f2937; /* Dark Gray for Sidebar */
--medium-gray-color: #6b7280;
--light-gray-color: #f3f4f6;
--surface-color: #ffffff; /* White */
--text-color: #111827;
--text-color-light: #f9fafb;
--border-color: #e5e7eb;
--header-height: 60px;
--sidebar-width: 250px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--light-gray-color);
color: var(--text-color);
line-height: 1.6;
font-size: 16px;
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
}
body::after {
content: "Lakhhna";
position: fixed;
bottom: 10px;
right: 10px;
opacity: 0.1;
font-size: 3rem;
color: var(--primary-color);
z-index: -1;
font-weight: 700;
}
a {
text-decoration: none;
color: var(--primary-color);
}
a:hover {
color: var(--primary-color-dark);
}
ul {
list-style: none;
}
img {
max-width: 100%;
height: auto;
}
input,
button,
select {
font-family: inherit;
font-size: inherit;
border-radius: 4px;
}
button {
cursor: pointer;
border: none;
padding: 0.6em 1.2em;
transition: background-color 0.2s ease, opacity 0.2s ease;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5em;
font-weight: 500;
border-radius: 6px;
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
}
.btn-primary {
background-color: var(--primary-color);
color: var(--surface-color);
}
.btn-primary:hover:not(:disabled) {
background-color: var(--primary-color-dark);
}
.btn-secondary {
background-color: var(--secondary-color);
color: var(--surface-color);
}
.btn-secondary:hover:not(:disabled) {
background-color: #059669;
}
.btn-danger {
background-color: var(--danger-color);
color: var(--surface-color);
}
.btn-danger:hover:not(:disabled) {
background-color: #dc2626;
}
.btn-warning {
background-color: var(--warning-color);
color: var(--dark-color);
}
.btn-warning:hover:not(:disabled) {
background-color: #d97706;
}
.spinner {
display: inline-block;
width: 1em;
height: 1em;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--surface-color);
animation: spin 1s ease-in-out infinite;
margin-left: 8px;
vertical-align: middle;
}
.btn.loading .spinner { display: inline-block; }
.btn:not(.loading) .spinner { display: none; }
.btn.loading > *:not(.spinner) {
visibility: hidden;
opacity: 0;
}
.btn.loading .spinner {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
visibility: visible;
opacity: 1;
}
.btn { position: relative; }
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Layout */
#app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
overflow-x: hidden;
}
/* Header */
header {
background-color: var(--surface-color);
color: var(--text-color);
padding: 0 15px;
height: var(--header-height);
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-color);
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
}
#menu-toggle {
background: none;
border: none;
font-size: 1.5rem;
color: var(--text-color);
padding: 5px;
display: block;
}
#mobile-logo {
font-size: 1.4rem;
font-weight: 700;
color: var(--primary-color);
display: block;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
header:has(#menu-toggle) #mobile-logo {
padding-left: 40px;
}
#user-profile {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-color);
margin-left: auto;
}
#user-profile i {
font-size: 1.8rem;
color: var(--medium-gray-color);
}
#user-greeting {
display: none;
font-size: 0.9rem;
color: var(--medium-gray-color);
}
/* Sidebar */
#sidebar {
background-color: var(--dark-color);
color: var(--text-color-light);
width: var(--sidebar-width);
height: 100%;
position: fixed;
top: 0;
left: calc(-1 * var(--sidebar-width));
padding-top: var(--header-height);
transition: left 0.3s ease-in-out;
z-index: 101;
overflow-y: auto;
}
#sidebar.open {
left: 0;
}
#sidebar nav ul {
padding: 20px 0;
}
#sidebar nav ul li a {
display: flex;
align-items: center;
padding: 12px 20px;
color: var(--text-color-light);
font-size: 0.95rem;
transition: background-color 0.2s ease;
border-left: 4px solid transparent;
}
#sidebar nav ul li a i {
width: 30px;
text-align: center;
margin-right: 10px;
font-size: 1.1em;
}
#sidebar nav ul li a:hover {
background-color: rgba(255, 255, 255, 0.1);
}
#sidebar nav ul li a.active {
background-color: var(--primary-color);
border-left-color: var(--surface-color);
font-weight: 600;
}
/* Sidebar Overlay */
#sidebar-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: none;
z-index: 100;
cursor: pointer;
}
#sidebar-overlay.show {
display: block;
}
/* Main Content Area */
#main-content {
margin-top: var(--header-height);
padding: 20px;
flex-grow: 1;
background-color: var(--light-gray-color);
position: relative;
}
/* Page Content Styling */
.page-content {
display: none;
background-color: var(--surface-color);
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
min-height: 300px;
animation: fadeIn 0.5s ease forwards;
}
.page-content.active-page {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.page-title {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 20px;
color: var(--primary-color);
border-bottom: 2px solid var(--primary-color-light);
padding-bottom: 10px;
}
.section-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 15px;
color: var(--dark-color);
padding-bottom: 5px;
border-bottom: 1px solid var(--border-color);
}
/* Auth Pages */
.auth-page {
max-width: 450px;
margin: 40px auto;
padding: 30px;
background-color: var(--surface-color);
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
text-align: center;
}
.auth-page h2 {
margin-bottom: 25px;
font-size: 1.8rem;
color: var(--primary-color);
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--medium-gray-color);
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 1rem;
}
.form-group input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
}
.auth-page button {
width: 100%;
margin-top: 10px;
padding: 12px;
font-size: 1.1rem;
}
.auth-link {
margin-top: 20px;
font-size: 0.9rem;
}
.auth-link a {
font-weight: 500;
cursor: pointer;
}
.validation-hint {
font-size: 0.8rem;
color: var(--medium-gray-color);
margin-top: 5px;
}
/* Messages */
.message-area {
margin-top: 15px;
min-height: 2em;
text-align: left;
}
.error-message, .success-message {
padding: 10px 15px;
border-radius: 4px;
margin-bottom: 15px;
font-size: 0.9rem;
text-align: left;
word-wrap: break-word;
}
.error-message {
background-color: #fee2e2;
color: #b91c1c;
border: 1px solid #fecaca;
}
.success-message {
background-color: #d1fae5;
color: #047857;
border: 1px solid #a7f3d0;
}
/* Dashboard */
.welcome-section {
background: linear-gradient(90deg, var(--primary-color), var(--primary-color-light));
color: var(--surface-color);
padding: 20px;
border-radius: 8px;
margin-bottom: 25px;
font-size: 1.2rem;
text-align: center;
}
.welcome-section strong { font-weight: 700; }
.stats-panel {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
margin-bottom: 25px;
}
.stat-card {
background-color: var(--surface-color);
padding: 20px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 15px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
border: 1px solid var(--border-color);
}
.stat-card .icon-wrapper {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--surface-color);
font-size: 1.4rem;
flex-shrink: 0;
}
.stat-card[data-stat="balance"] .icon-wrapper { background-color: var(--primary-color); }
.stat-card[data-stat="spins"] .icon-wrapper { background-color: var(--secondary-color); }
.stat-card[data-stat="tokens"] .icon-wrapper { background-color: var(--danger-color); }
.stat-card[data-stat="scratches"] .icon-wrapper { background-color: var(--warning-color); }
.stat-card .info {
display: flex;
flex-direction: column;
}
.stat-card .label {
font-size: 0.9rem;
color: var(--medium-gray-color);
margin-bottom: 4px;
}
.stat-card .value {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-color);
}
.spin-action-section {
background-color: var(--primary-color-light);
color: var(--dark-color);
padding: 25px;
border-radius: 8px;
text-align: center;
margin-bottom: 25px;
}
.spin-action-section h3 { margin-bottom: 10px; font-size: 1.3rem;}
.spin-action-section p { margin-bottom: 20px; font-size: 1rem;}
.feature-grid {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
margin-bottom: 25px;
}
.feature-card {
background-color: var(--surface-color);
padding: 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.feature-card .icon-wrapper {
width: 45px;
height: 45px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: var(--surface-color);
font-size: 1.3rem;
margin-bottom: 15px;
}
.feature-card[data-feature="daily"] .icon-wrapper { background-color: var(--secondary-color); }
.feature-card[data-feature="refer"] .icon-wrapper { background-color: var(--primary-color); }
.feature-card[data-feature="redeem"] .icon-wrapper { background-color: var(--warning-color); }
.feature-card[data-feature="history"] .icon-wrapper { background-color: var(--medium-gray-color); }
.feature-card[data-feature="spin"] .icon-wrapper { background-color: var(--primary-color); }
.feature-card[data-feature="slots"] .icon-wrapper { background-color: var(--danger-color); }
.feature-card[data-feature="scratch"] .icon-wrapper { background-color: var(--warning-color); }
.feature-card h4 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-color);
}
.feature-card p {
font-size: 0.9rem;
color: var(--medium-gray-color);
margin-bottom: 15px;
flex-grow: 1;
}
.feature-card .btn {
align-self: flex-start;
padding: 0.5rem 1rem;
font-size: 0.85rem;
cursor: pointer;
}
.activity-log-section {
margin-top: 30px;
}
.activity-log-section h3 {
font-size: 1.3rem;
margin-bottom: 15px;
}
.activity-list {
margin-bottom: 15px;
min-height: 50px;
}
.activity-item {
display: flex;
align-items: center;
gap: 15px;
padding: 12px 0;
border-bottom: 1px solid var(--border-color);
font-size: 0.9rem;
}
.activity-item:last-child {
border-bottom: none;
}
.activity-item .icon-wrapper {
width: 35px;
height: 35px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--surface-color);
font-size: 1rem;
flex-shrink: 0;
}
.activity-item[data-type^="spin"] .icon-wrapper { background-color: var(--primary-color); }
.activity-item[data-type^="slots"] .icon-wrapper { background-color: var(--danger-color); }
.activity-item[data-type^="scratch"] .icon-wrapper { background-color: var(--warning-color); }
.activity-item[data-type="daily_bonus"] .icon-wrapper { background-color: var(--secondary-color); }
.activity-item[data-type="redeem_request"] .icon-wrapper { background-color: var(--warning-color); }
.activity-item[data-type="register"] .icon-wrapper { background-color: var(--medium-gray-color); }
.activity-item[data-type="login"] .icon-wrapper { background-color: var(--medium-gray-color); }
.activity-item[data-type="logout"] .icon-wrapper { background-color: var(--medium-gray-color); }
.activity-item[data-type="daily_reset"] .icon-wrapper { background-color: var(--primary-color-light); }
.activity-item[data-type="default"] .icon-wrapper { background-color: var(--medium-gray-color); }
.activity-item .description {
flex-grow: 1;
color: var(--text-color);
word-break: break-word;
}
.activity-item .timestamp {
font-size: 0.8rem;
color: var(--medium-gray-color);
white-space: nowrap;
margin-left: auto;
padding-left: 10px;
}
.view-all-link {
display: block;
text-align: right;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
}
/* Spin Wheel Page */
#spin-wheel-content { text-align: center; }
.spin-wheel-container {
position: relative;
width: 300px;
height: 300px;
margin: 30px auto;
display: flex;
align-items: center;
justify-content: center;
}
.wheel {
width: 100%;
height: 100%;
border-radius: 50%;
border: 10px solid var(--dark-color);
position: relative;
overflow: hidden;
background-color: var(--light-gray-color);
}
.wheel-label {
position: absolute;
transform-origin: center center;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
font-size: 11px;
font-weight: bold;
color: var(--dark-color);
padding-left: 50px;
writing-mode: vertical-rl;
text-orientation: mixed;
transform: translate(-50%, -50%);
}
.wheel-pointer {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%) rotate(180deg);
width: 0;
height: 0;
border-left: 15px solid transparent;
border-right: 15px solid transparent;
border-bottom: 30px solid var(--danger-color);
z-index: 10;
}
.wheel-center {
position: absolute;
width: 40px;
height: 40px;
background-color: var(--dark-color);
border-radius: 50%;
border: 5px solid var(--surface-color);
z-index: 5;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#spin-info, #spin-result {
margin-top: 20px;
font-size: 1.1rem;
min-height: 1.5em;
}
#spin-info { font-weight: 600; }
#spin-result { font-weight: 700; color: var(--primary-color); }
#spin-now-btn { margin-top: 20px; }
/* Timer and Link Styles */
.spin-cooldown {
margin-top: 20px;
font-size: 1rem;
color: var(--medium-gray-color);
text-align: center;
}
.spin-cooldown-link {
display: inline-block;
margin-top: 10px;
padding: 10px 15px;
background-color: var(--primary-color);
color: white;
border-radius: 5px;
text-decoration: none;
font-weight: 500;
transition: background-color 0.2s;
}
.spin-cooldown-link:hover {
background-color: var(--primary-color-dark);
}
.spin-cooldown-link.disabled {
background-color: var(--medium-gray-color);
cursor: not-allowed;
pointer-events: none;
}
.spin-cooldown-timer {
font-weight: 600;
color: var(--primary-color);
}
/* Slot Machine Page */
#slot-machine-content { text-align: center; }
.slots-container {
background-color: var(--light-gray-color);
padding: 30px 20px;
border-radius: 10px;
border: 5px solid var(--dark-color);
display: inline-block;
margin: 30px auto;
box-shadow: inset 0 0 10px rgba(0,0,0,0.2);
}
#slots-info {
margin-bottom: 20px;
font-size: 1rem;
color: var(--medium-gray-color);
}
.slots-reels {
display: flex;
gap: 10px;
background-color: var(--surface-color);
padding: 15px;
border-radius: 5px;
border: 2px solid #ccc;
margin-bottom: 25px;
height: 150px;
overflow: hidden;
position: relative;
}
.reel {
width: 70px;
background-color: #eee;
border: 1px solid #ddd;
overflow: hidden;
position: relative;
height: 100%;
}
.reel-symbols {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.reel-symbol {
font-size: 2.5rem;
height: 50px;
line-height: 50px;
text-align: center;
display: block;
border-bottom: 1px dashed #ccc;
}
.reel-symbol:last-child { border-bottom: none; }
#slots-result {
margin-top: 20px;
font-size: 1.2rem;
font-weight: 700;
min-height: 1.5em;
color: var(--primary-color);
}
#play-slots-btn { margin-top: 10px; }
/* Scratch Cards Page */
#scratch-cards-content { text-align: center; }
.scratch-card-container {
margin: 30px auto;
position: relative;
display: inline-block;
}
#scratch-info {
margin-bottom: 15px;
font-size: 1rem;
color: var(--medium-gray-color);
}
.scratch-card-area {
width: 300px;
height: 150px;
background-color: var(--primary-color-light);
color: var(--dark-color);
border: 3px dashed var(--primary-color);
border-radius: 8px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
overflow: hidden;
}
.scratch-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #bdc3c7, #95a5a6);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 1.2rem;
font-weight: 600;
color: var(--dark-color);
text-align: center;
transition: opacity 0.5s ease;
z-index: 2;
}
.scratch-overlay.scratched {
opacity: 0;
pointer-events: none;
}
.scratch-result {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
font-weight: 700;
color: var(--primary-color);
background-color: #e0f2fe;
opacity: 0;
transition: opacity 0.3s ease 0.2s;
z-index: 1;
}
.scratch-result.revealed {
opacity: 1;
}
#scratch-status {
margin-top: 20px;
font-size: 1rem;
min-height: 1.5em;
}
#scratch-now-btn { margin-top: 20px; }
/* Placeholder Page Style */
.placeholder-page {
text-align: center;
padding: 50px 20px;
}
.placeholder-page .placeholder-icon {
font-size: 5rem;
color: var(--primary-color-light);
margin-bottom: 25px;
}
.placeholder-page h2 {
font-size: 1.8rem;
margin-bottom: 15px;
color: var(--primary-color);
}
.placeholder-page p {
font-size: 1rem;
color: var(--medium-gray-color);
max-width: 500px;
margin: 0 auto 20px auto;
}
.placeholder-page button {
margin-top: 15px;
}
.status-message {
margin-top: 15px;
font-size: 0.9rem;
min-height: 1.3em;
}
#referral-code-display {
background-color: var(--light-gray-color);
padding: 15px;
border-radius: 6px;
border: 1px dashed var(--medium-gray-color);
font-size: 1.2rem;
font-weight: 600;
color: var(--primary-color);
margin: 20px auto;
display: inline-block;
word-break: break-all;
min-width: 150px;
min-height: 1.5em;
}
.info-item {
font-size: 1rem;
margin-bottom: 10px;
color: var(--medium-gray-color);
text-align: left;
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
.info-item strong {
color: var(--primary-color);
margin-right: 8px;
display: inline-block;
width: 120px;
}
.info-item span {
color: var(--text-color);
font-weight: 500;
}
/* Withdrawal Method Styles */
.withdrawal-methods {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.withdrawal-method {
flex: 1;
min-width: 200px;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 15px;
cursor: pointer;
transition: all 0.3s ease;
}
.withdrawal-method.selected {
border-color: var(--primary-color);
background-color: rgba(59, 130, 246, 0.05);
}
.withdrawal-method-logo {
width: 60px;
height: 30px;
object-fit: contain;
margin-bottom: 10px;
}
/* Screenshot Section */
.screenshot-section {
margin-top: 30px;
text-align: center;
padding: 20px;
background-color: var(--light-gray-color);
border-radius: 8px;
display: none;
}
.screenshot-instructions {
margin-bottom: 15px;
}
.screenshot-link {
display: inline-block;
padding: 10px 20px;
background-color: var(--secondary-color);
color: white;
border-radius: 5px;
text-decoration: none;
font-weight: 500;
transition: background-color 0.2s;
}
.screenshot-link:hover {
background-color: #059669;
}
/* Redeem Points Page */
#redeem-content .page-title { margin-bottom: 10px; }
#redeem-points-balance-container {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 25px;
color: var(--secondary-color);
text-align: center;
}
#withdraw-form {
max-width: 500px;
margin: 0 auto;
text-align: left;
background-color: var(--light-gray-color);
padding: 20px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
#withdraw-form .message-area {
margin-bottom: 15px;
margin-top: 0;
}
#withdraw-form .form-group label {
margin-bottom: 5px;
}
#withdraw-form input[type="number"],
#withdraw-form input[type="text"] {
width: 100%;
padding: 10px;
margin-bottom: 15px;
border: 1px solid var(--border-color);
background-color: var(--surface-color);
border-radius: 4px;
}
.simulation-warning {
font-size: 0.85rem;
color: var(--danger-color);
margin-top: -10px;
margin-bottom: 15px;
font-weight: 500;
}
#withdraw-form button {
width: 100%;
margin-top: 10px;
}
/* History Page */
#history-content .page-title { margin-bottom: 15px; }
#history-content > p {
margin-bottom: 20px;
color: var(--medium-gray-color);
text-align: center;
}
#full-activity-list {
max-height: 60vh;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
background-color: #fff;
}
#full-activity-list .activity-item:first-child {
border-top: none;
}
#full-activity-list .activity-item:last-child {
border-bottom: none;
}
/* Footer */
footer {
text-align: center;
padding: 15px;
background-color: var(--dark-color);
color: white;
font-size: 0.9rem;
}
/* Responsive Adjustments */
/* Tablet (>= 768px) */
@media (min-width: 768px) {
#mobile-logo {
position: static;
transform: none;
padding-left: 0;
}
#user-greeting {
display: inline;
}
.stats-panel {
grid-template-columns: repeat(2, 1fr);
}
.feature-grid {
grid-template-columns: repeat(2, 1fr);
}
.auth-page {
margin-top: 60px;
}
.spin-wheel-container { width: 350px; height: 350px; }
.wheel-label { font-size: 12px; padding-left: 60px; }
.slots-reels { height: 180px; }
.reel { width: 80px; }
.reel-symbol { font-size: 3rem; height: 60px; line-height: 60px; }
.scratch-card-area { width: 350px; height: 180px; }
}
/* Desktop (>= 992px) */
@media (min-width: 992px) {
#app-container {
display: grid;
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-rows: var(--header-height) 1fr;
height: 100vh;
overflow: hidden;
}
#sidebar {
position: static;
grid-row: 1 / 3;
grid-column: 1 / 2;
padding-top: 20px;
height: 100vh;
left: 0 !important;
z-index: 1;
transition: none;
border-right: 1px solid var(--border-color);
}
#sidebar nav { padding-top: 0px; }
header {
position: static;
grid-row: 1 / 2;
grid-column: 2 / 3;
justify-content: flex-end;
padding-right: 30px;
border-bottom: 1px solid var(--border-color);
}
#menu-toggle, #mobile-logo {
display: none;
}
#sidebar-overlay {
display: none !important;
}
#main-content {
grid-row: 2 / 3;
grid-column: 2 / 3;
margin-top: 0;
overflow-y: auto;
padding: 30px;
}
.stats-panel {
grid-template-columns: repeat(4, 1fr);
}
.feature-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Large Desktop (>= 1200px) */
@media (min-width: 1200px) {
.feature-grid {
grid-template-columns: repeat(4, 1fr);
}
}
/* Helper classes */
.hidden { display: none !important; }
/* --- Login State Styling --- */
/* Default (Logged Out) */
.logged-in-item { display: none; }
.logged-out-item { display: block; }
/* When Logged In */
body.logged-in .logged-in-item { display: block; }
body.logged-in .logged-out-item { display: none; }
/* Adjust sidebar links display */
#sidebar nav ul li.logged-in-item a { display: none; }
#sidebar nav ul li.logged-out-item a { display: flex; }
body.logged-in #sidebar nav ul li.logged-in-item a { display: flex; }
body.logged-in #sidebar nav ul li.logged-out-item a { display: none; }
</style>
</head>
<body class="logged-out">
<div id="app-container">
<!-- Header -->
<header>
<button id="menu-toggle" aria-label="Toggle Menu">
<i class="fas fa-bars"></i>
</button>
<span id="mobile-logo">SpinEarn by Lakhhna</span>
<div id="user-profile">
<span id="user-greeting">Hi, Guest!</span>
<i class="fas fa-user-circle"></i>
</div>
</header>
<!-- Sidebar -->
<aside id="sidebar">
<div style="padding: 15px 20px; text-align: center; border-bottom: 1px solid rgba(255,255,255,0.1);">
<h2 style="color: var(--primary-color); font-weight: 700; margin: 0;">SpinEarn</h2>
<p style="color: var(--text-color-light); font-size: 0.8rem; margin-top: 5px;">by Lakhhna</p>
</div>
<nav>
<ul>
<!-- Logged Out Links -->
<li class="logged-out-item">
<a href="#login" data-page="login-content" class="sidebar-link">
<i class="fas fa-sign-in-alt"></i> Login
</a>
</li>
<li class="logged-out-item">
<a href="#register" data-page="register-content" class="sidebar-link">
<i class="fas fa-user-plus"></i> Register
</a>
</li>
<!-- Logged In Links -->
<li class="logged-in-item">
<a href="#dashboard" data-page="dashboard-content" class="sidebar-link active">
<i class="fas fa-tachometer-alt"></i> Dashboard
</a>
</li>
<li class="logged-in-item">
<a href="#spin-wheel" data-page="spin-wheel-content" class="sidebar-link">
<i class="fas fa-bullseye"></i> Spin Wheel
</a>
</li>
<li class="logged-in-item">
<a href="#slot-machine" data-page="slot-machine-content" class="sidebar-link">
<i class="fas fa-dice-three"></i> Slot Machine
</a>
</li>
<li class="logged-in-item">
<a href="#scratch-cards" data-page="scratch-cards-content" class="sidebar-link">
<i class="fas fa-clone"></i> Scratch Cards
</a>
</li>
<li class="logged-in-item">
<a href="#daily-bonus" data-page="daily-bonus-content" class="sidebar-link">
<i class="fas fa-calendar-check"></i> Daily Bonus
</a>
</li>
<li class="logged-in-item">
<a href="#refer-earn" data-page="refer-earn-content" class="sidebar-link">
<i class="fas fa-user-friends"></i> Refer & Earn
</a>
</li>
<li class="logged-in-item">
<a href="#redeem" data-page="redeem-content" class="sidebar-link">
<i class="fas fa-gift"></i> Redeem Points
</a>
</li>
<li class="logged-in-item">
<a href="#history" data-page="history-content" class="sidebar-link">
<i class="fas fa-history"></i> History
</a>
</li>
<li class="logged-in-item">
<a href="#profile" data-page="profile-content" class="sidebar-link">
<i class="fas fa-user-edit"></i> Profile
</a>
</li>
<li class="logged-in-item">
<a href="#logout" id="logout-link" class="sidebar-link">
<i class="fas fa-sign-out-alt"></i> Logout
</a>
</li>
</ul>
</nav>
</aside>
<!-- Sidebar Overlay -->
<div id="sidebar-overlay"></div>
<!-- Main Content -->
<main id="main-content">
<!-- Login Page -->
<div id="login-content" class="page-content auth-page active-page">
<h2>Login</h2>
<form id="login-form">
<div class="form-group">
<label for="login-username">Username</label>
<input type="text" id="login-username" required autocomplete="username">
</div>
<div class="form-group">
<label for="login-password">Password</label>
<input type="password" id="login-password" required autocomplete="current-password">
</div>
<div id="login-error-message" class="message-area"></div>
<button type="submit" id="login-button" class="btn btn-primary">
<span>Login</span>
<span class="spinner"></span>
</button>
</form>
<p class="auth-link">Don't have an account? <a href="#register" class="nav-link" data-page="register-content">Register here</a></p>
</div>
<!-- Register Page -->
<div id="register-content" class="page-content auth-page">
<h2>Register</h2>
<form id="register-form">
<div class="form-group">
<label for="register-username">Username</label>
<input type="text" id="register-username" required minlength="4" pattern="^[a-zA-Z0-9_]+$" autocomplete="username">
<p class="validation-hint">4+ characters, letters, numbers, underscores only.</p>
</div>
<div class="form-group">
<label for="register-password">Password</label>
<input type="password" id="register-password" required minlength="6" autocomplete="new-password">
<p class="validation-hint">Minimum 6 characters.</p>
</div>
<div class="form-group">
<label for="register-confirm-password">Confirm Password</label>
<input type="password" id="register-confirm-password" required autocomplete="new-password">
</div>
<div id="register-error-message" class="message-area"></div>
<div id="register-success-message" class="message-area"></div>
<button type="submit" id="register-button" class="btn btn-primary">
<span>Register</span>
<span class="spinner"></span>
</button>
</form>
<p class="auth-link">Already have an account? <a href="#login" class="nav-link" data-page="login-content">Login here</a></p>
</div>
<!-- Dashboard Page -->
<div id="dashboard-content" class="page-content">
<div class="welcome-section">
Welcome Back, <strong id="dashboard-username">User</strong>!<br>
<small>Powered by Lakhhna</small>
</div>
<h2 class="section-title">Your Stats</h2>
<div class="stats-panel">
<div class="stat-card" data-stat="balance">
<div class="icon-wrapper"><i class="fas fa-coins"></i></div>
<div class="info">
<span class="label">Balance</span>
<span class="value" id="balance-value">--- pts</span>
</div>
</div>
<div class="stat-card" data-stat="spins">
<div class="icon-wrapper"><i class="fas fa-sync-alt"></i></div>
<div class="info">
<span class="label">Spins Left</span>
<span class="value" id="spins-left-value">--</span>
</div>
</div>
<div class="stat-card" data-stat="tokens">
<div class="icon-wrapper"><i class="fas fa-ticket-alt"></i></div>
<div class="info">
<span class="label">Slot Tokens</span>
<span class="value" id="slot-tokens-value">--</span>
</div>
</div>
<div class="stat-card" data-stat="scratches">
<div class="icon-wrapper"><i class="fas fa-paint-roller"></i></div>
<div class="info">
<span class="label">Scratch Cards</span>
<span class="value" id="scratch-cards-value">--</span>
</div>
</div>
</div>
<div class="spin-action-section">
<h3>Ready for More Points?</h3>
<p>Try your luck on the Spin Wheel now!</p>
<button class="btn btn-secondary nav-link" data-page="spin-wheel-content">
<i class="fas fa-bullseye"></i> Go to Spin Wheel
</button>
</div>
<h2 class="section-title">Explore Features</h2>
<div class="feature-grid">
<div class="feature-card" data-feature="spin">
<div class="icon-wrapper"><i class="fas fa-bullseye"></i></div>
<h4>Spin Wheel</h4>
<p>Test your luck and win points on the wheel.</p>
<button class="btn btn-primary nav-link" data-page="spin-wheel-content">Spin Now</button>
</div>
<div class="feature-card" data-feature="slots">
<div class="icon-wrapper"><i class="fas fa-dice-three"></i></div>
<h4>Slot Machine</h4>
<p>Use tokens for a chance at bigger prizes.</p>
<button class="btn btn-primary nav-link" data-page="slot-machine-content">Play Slots</button>
</div>
<div class="feature-card" data-feature="scratch">
<div class="icon-wrapper"><i class="fas fa-clone"></i></div>
<h4>Scratch Cards</h4>
<p>Scratch daily cards for instant wins.</p>
<button class="btn btn-primary nav-link" data-page="scratch-cards-content">Scratch Now</button>
</div>
<div class="feature-card" data-feature="daily">
<div class="icon-wrapper"><i class="fas fa-calendar-check"></i></div>
<h4>Daily Bonus</h4>
<p>Claim your free points every single day.</p>
<button class="btn btn-primary nav-link" data-page="daily-bonus-content">Claim Bonus</button>
</div>
<div class="feature-card" data-feature="refer">
<div class="icon-wrapper"><i class="fas fa-user-friends"></i></div>
<h4>Refer & Earn</h4>
<p>Invite friends and earn bonus points together.</p>
<button class="btn btn-primary nav-link" data-page="refer-earn-content">Refer Friends</button>
</div>
<div class="feature-card" data-feature="redeem">
<div class="icon-wrapper"><i class="fas fa-gift"></i></div>
<h4>Redeem Points</h4>
<p>Withdraw your earned points (simulation).</p>
<button class="btn btn-primary nav-link" data-page="redeem-content">Redeem Now</button>
</div>
<div class="feature-card" data-feature="history">
<div class="icon-wrapper"><i class="fas fa-history"></i></div>
<h4>Activity History</h4>
<p>See all your recent earnings and actions.</p>
<button class="btn btn-primary nav-link" data-page="history-content">View History</button>
</div>
<div class="feature-card" data-feature="profile">
<div class="icon-wrapper"><i class="fas fa-user-edit"></i></div>
<h4>Profile</h4>
<p>View your account details and settings.</p>
<button class="btn btn-primary nav-link" data-page="profile-content">View Profile</button>
</div>
</div>
<div class="activity-log-section">
<h3>Recent Activity</h3>
<div id="dashboard-activity-list" class="activity-list">
<p>Loading activity...</p>
</div>
<a href="#history" class="view-all-link nav-link" data-page="history-content">View All Activity <i class="fas fa-arrow-right"></i></a>
</div>
</div>
<!-- Spin Wheel Page -->
<div id="spin-wheel-content" class="page-content">
<h2 class="page-title">Spin the Wheel!</h2>
<div class="spin-wheel-container">
<div class="wheel" id="spin-wheel">
<!-- Segments and labels generated by JS -->
</div>
<div class="wheel-pointer"></div>
<div class="wheel-center"></div>
</div>
<div id="spin-info">Spins Left: <span id="spin-page-spins-left">--</span></div>
<div id="spin-result">Click "Spin Now" to play!</div>
<button id="spin-now-btn" class="btn btn-secondary">
<span><i class="fas fa-sync-alt"></i> Spin Now</span>
<span class="spinner"></span>
</button>
<div class="spin-cooldown">
<p>Next spin available in: <span class="spin-cooldown-timer" id="spin-timer">Ready!</span></p>
<a href="https://ernspin.blogspot.com" class="spin-cooldown-link" id="spin-link" target="_blank">
Visit Our Blog
</a>
</div>
</div>
<!-- Slot Machine Page -->
<div id="slot-machine-content" class="page-content">
<h2 class="page-title">Slot Machine</h2>
<div class="slots-container">
<div id="slots-info">Cost: 1 Token | Your Tokens: <span id="slot-page-tokens">--</span></div>
<div class="slots-reels">
<div class="reel"><div class="reel-symbols" id="reel1"><!-- Symbols by JS --></div></div>
<div class="reel"><div class="reel-symbols" id="reel2"><!-- Symbols by JS --></div></div>
<div class="reel"><div class="reel-symbols" id="reel3"><!-- Symbols by JS --></div></div>
</div>
<div id="slots-result">Ready to play?</div>
<button id="play-slots-btn" class="btn btn-danger">
<span><i class="fas fa-play"></i> Play (1 Token)</span>
<span class="spinner"></span>
</button>
</div>
</div>
<!-- Scratch Cards Page -->
<div id="scratch-cards-content" class="page-content">
<h2 class="page-title">Scratch & Win</h2>
<div class="scratch-card-container">
<div id="scratch-info">Cards Left Today: <span id="scratch-page-cards-left">--</span></div>
<div class="scratch-card-area" id="scratch-card-area">
<div class="scratch-result" id="scratch-card-result">? Pts</div>
<div class="scratch-overlay" id="scratch-card-overlay">
<i class="fas fa-hand-pointer fa-2x" style="margin-bottom: 10px;"></i>
<p>Click or Tap to Scratch!</p>
</div>
</div>
<div id="scratch-status">Click the card or button below to scratch!</div>
<button id="scratch-now-btn" class="btn btn-warning">
<span><i class="fas fa-paint-roller"></i> Scratch Card</span>
<span class="spinner"></span>
</button>
</div>
</div>
<!-- Daily Bonus Page -->
<div id="daily-bonus-content" class="page-content placeholder-page">
<i class="fas fa-calendar-check placeholder-icon"></i>
<h2 class="page-title">Daily Bonus</h2>
<p id="daily-bonus-message">Check back every day to claim your free bonus points!</p>
<button id="claim-bonus-btn" class="btn btn-secondary">
<span><i class="fas fa-gift"></i> Claim Today's Bonus</span>
<span class="spinner"></span>
</button>
<div id="daily-bonus-status" class="message-area" style="max-width: 400px; margin: 15px auto 0 auto;"></div>
</div>
<!-- Refer & Earn Page -->
<div id="refer-earn-content" class="page-content placeholder-page">
<i class="fas fa-user-friends placeholder-icon"></i>
<h2 class="page-title">Refer & Earn</h2>
<p>Share your referral code with friends. When they sign up using your code (feature not fully implemented), you both might get bonus points!</p>
<p>Your Referral Code:</p>
<div id="referral-code-display">LOADING...</div>
<button id="copy-code-btn" class="btn btn-primary">
<i class="fas fa-copy"></i> Copy Code
</button>
<div id="copy-status" class="message-area" style="max-width: 400px; margin: 15px auto 0 auto;"></div>
<p style="margin-top: 20px; font-size: 0.8rem;">Note: Referral signup tracking and reward distribution are simulated for this demonstration.</p>
</div>
<!-- Redeem Points Page -->
<div id="redeem-content" class="page-content">
<h2 class="page-title">Redeem Points (Withdrawal Simulation)</h2>
<p id="redeem-points-balance-container">Your current balance: <span id="redeem-points-balance">--- pts</span></p>
<p style="text-align:center; margin-bottom: 20px; color: var(--medium-gray-color);">Use the form below to simulate requesting a withdrawal. Minimum 100 points.</p>
<form id="withdraw-form">
<div id="withdraw-error-message" class="message-area"></div>
<div id="withdraw-success-message" class="message-area"></div>
<div class="form-group">
<label for="withdraw-amount">Amount (Points)</label>
<input type="number" id="withdraw-amount" min="100" required placeholder="e.g., 500">
</div>
<div class="form-group">
<label>Withdrawal Method</label>
<div class="withdrawal-methods">
<div class="withdrawal-method" data-method="easypaisa">
<img src="https://www.easypaisa.com.pk/wp-content/themes/easypaisa/images/logo.svg"
alt="EasyPaisa" class="withdrawal-method-logo">
<p>EasyPaisa</p>
</div>
<div class="withdrawal-method" data-method="jazzcash">
<img src="https://www.jazzcash.com.pk/images/jazzcash-logo.svg"
alt="JazzCash" class="withdrawal-method-logo">
<p>JazzCash</p>
</div>
</div>
<input type="hidden" id="withdraw-method" required>
</div>
<div class="form-group">
<label for="withdraw-details">Account Number/Phone Number</label>
<input type="text" id="withdraw-details" required placeholder="e.g., 03001234567">
<p class="simulation-warning"><strong>Warning:</strong> Do NOT enter real financial information. This is only a simulation.</p>
</div>
<button type="submit" id="withdraw-button" class="btn btn-primary">
<span>Request Withdrawal</span>
<span class="spinner"></span>
</button>
</form>
<div class="screenshot-section" id="screenshot-section">
<h3>Withdrawal Successful!</h3>
<p class="screenshot-instructions">Please submit screenshot proof of payment</p>
<a href="https://ernspinwithdrwal.blogspot.com" class="screenshot-link" target="_blank">
Submit Screenshot Here
</a>
</div>
</div>
<!-- History Page -->
<div id="history-content" class="page-content">
<h2 class="page-title">Activity History</h2>
<p>Here is a list of all your recorded activities within the app.</p>
<div id="full-activity-list" class="activity-list">
<p>Loading history...</p>
</div>
</div>
<!-- Profile Page -->
<div id="profile-content" class="page-content placeholder-page">
<i class="fas fa-user-edit placeholder-icon"></i>
<h2 class="page-title">Profile</h2>
<div class="info-item"><strong>Username:</strong> <span id="profile-username">---</span></div>
<div class="info-item"><strong>Points Balance:</strong> <span id="profile-points">---</span></div>
<div class="info-item"><strong>Referral Code:</strong> <span id="profile-ref-code">---</span></div>
<div class="info-item"><strong>Member Since:</strong> <span id="profile-member-since">---</span></div>
<div class="info-item"><strong>Last Login:</strong> <span id="profile-last-login">---</span></div>
<button class="btn btn-primary" disabled style="margin-top: 20px;">Change Password (Not Implemented)</button>
<p style="margin-top: 20px; font-size: 0.8rem;">More profile settings could be added here.</p>
</div>
</main>
<!-- Footer -->
<footer>
© 2023 SpinEarn - Created by Lakhhna
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- Configuration ---
const API_URL = 'api.php';
const spinWheelSegments = [
{ value: 10, color: '#A7F3D0' }, { value: 50, color: '#BAE6FD' },
{ value: 0, color: '#FECACA' }, { value: 100, color: '#FED7AA' },
{ value: 25, color: '#D1FAE5' }, { value: 5, color: '#E0E7FF' },
{ value: 200, color: '#FEF08A' }, { value: 0, color: '#FECACA' },
{ value: 15, color: '#CFFAFE' }, { value: 75, color: '#FBCFE8' }
];
const numSegments = spinWheelSegments.length;
const segmentAngle = 360 / numSegments;
const slotSymbols = ['🍒', '🍋', '🍊', '🍉', '⭐', '🔔', '7'];
const reelHeight = 50;
const numSymbolsPerReel = 30;
const MAX_DASHBOARD_ACTIVITIES = 5;
// --- State Variables ---
let isLoggedIn = false;
let currentUser = null;
let currentPage = 'login-content';
let isSpinningWheel = false;
let isSpinningSlots = false;
let isScratching = false;
let isProcessingAuth = false;
let isClaimingBonus = false;
let isWithdrawing = false;
let currentWheelRotation = 0;
let spinCooldown = 0;
let spinTimerInterval = null;
// --- DOM Element Selectors ---
const body = document.body;
const appContainer = document.getElementById('app-container');
const header = document.querySelector('header');
const menuToggle = document.getElementById('menu-toggle');
const userGreeting = document.getElementById('user-greeting');
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
const sidebarLinks = document.querySelectorAll('.sidebar-link');
const navLinks = document.querySelectorAll('.nav-link');
const mainContent = document.getElementById('main-content');
const pageContents = document.querySelectorAll('.page-content');
// Auth Elements
const loginForm = document.getElementById('login-form');
const loginUsernameInput = document.getElementById('login-username');
const loginPasswordInput = document.getElementById('login-password');
const loginButton = document.getElementById('login-button');
const loginErrorMessage = document.getElementById('login-error-message');
const registerForm = document.getElementById('register-form');
const registerUsernameInput = document.getElementById('register-username');
const registerPasswordInput = document.getElementById('register-password');
const registerConfirmPasswordInput = document.getElementById('register-confirm-password');
const registerButton = document.getElementById('register-button');
const registerErrorMessage = document.getElementById('register-error-message');
const registerSuccessMessage = document.getElementById('register-success-message');
const logoutLink = document.getElementById('logout-link');
// Dashboard Elements
const dashboardUsername = document.getElementById('dashboard-username');
const balanceValue = document.getElementById('balance-value');
const spinsLeftValue = document.getElementById('spins-left-value');
const slotTokensValue = document.getElementById('slot-tokens-value');
const scratchCardsValue = document.getElementById('scratch-cards-value');
const dashboardActivityList = document.getElementById('dashboard-activity-list');
// Spin Wheel Elements
const spinWheelElement = document.getElementById('spin-wheel');
const spinPageSpinsLeft = document.getElementById('spin-page-spins-left');
const spinResult = document.getElementById('spin-result');
const spinNowBtn = document.getElementById('spin-now-btn');
const spinTimer = document.getElementById('spin-timer');
const spinLink = document.getElementById('spin-link');
// Slot Machine Elements
const slotPageTokens = document.getElementById('slot-page-tokens');
const reel1Symbols = document.getElementById('reel1');
const reel2Symbols = document.getElementById('reel2');
const reel3Symbols = document.getElementById('reel3');
const slotsResult = document.getElementById('slots-result');
const playSlotsBtn = document.getElementById('play-slots-btn');
// Scratch Card Elements
const scratchPageCardsLeft = document.getElementById('scratch-page-cards-left');
const scratchCardArea = document.getElementById('scratch-card-area');
const scratchCardOverlay = document.getElementById('scratch-card-overlay');
const scratchCardResult = document.getElementById('scratch-card-result');
const scratchStatus = document.getElementById('scratch-status');
const scratchNowBtn = document.getElementById('scratch-now-btn');
// Daily Bonus Elements
const dailyBonusMessage = document.getElementById('daily-bonus-message');
const claimBonusBtn = document.getElementById('claim-bonus-btn');
const dailyBonusStatus = document.getElementById('daily-bonus-status');
// Refer & Earn Elements
const referralCodeDisplay = document.getElementById('referral-code-display');
const copyCodeBtn = document.getElementById('copy-code-btn');
const copyStatus = document.getElementById('copy-status');
// Redeem Points Elements
const redeemPointsBalance = document.getElementById('redeem-points-balance');
const withdrawForm = document.getElementById('withdraw-form');
const withdrawAmountInput = document.getElementById('withdraw-amount');
const withdrawMethodSelect = document.getElementById('withdraw-method');
const withdrawDetailsInput = document.getElementById('withdraw-details');
const withdrawButton = document.getElementById('withdraw-button');
const withdrawErrorMessage = document.getElementById('withdraw-error-message');
const withdrawSuccessMessage = document.getElementById('withdraw-success-message');
const screenshotSection = document.getElementById('screenshot-section');
// History Elements
const fullActivityList = document.getElementById('full-activity-list');
// Profile Elements
const profileUsername = document.getElementById('profile-username');
const profilePoints = document.getElementById('profile-points');
const profileRefCode = document.getElementById('profile-ref-code');
const profileMemberSince = document.getElementById('profile-member-since');
const profileLastLogin = document.getElementById('profile-last-login');
// --- API Helper Function ---
async function apiCall(action, data = {}, method = 'POST') {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
},
};
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
data.action = action;
options.body = JSON.stringify(data);
}
try {
const response = await fetch(API_URL, options);
const contentType = response.headers.get('content-type');
if (!response.ok) {
let errorData;
if (contentType && contentType.includes('application/json')) {
errorData = await response.json();
}
throw new Error(errorData?.message || response.statusText || `HTTP error! status: ${response.status}`);
}
if (response.status === 204 || !contentType || !contentType.includes('application/json')) {
return { success: true };
}
return await response.json();
} catch (error) {
console.error(`API call failed for action "${action}":`, error);
return { success: false, message: error.message || 'An unknown network error occurred.' };
}
}
// --- Core UI Functions ---
function updateUI() {
if (isLoggedIn && currentUser) {
body.classList.remove('logged-out');
body.classList.add('logged-in');
userGreeting.textContent = `Hi, ${currentUser.username}!`;
// Update common data displays
dashboardUsername.textContent = currentUser.username;
balanceValue.textContent = `${currentUser.points?.toLocaleString() ?? '0'} pts`;
spinsLeftValue.textContent = currentUser.spins_left ?? '0';
slotTokensValue.textContent = currentUser.slot_tokens ?? '0';
scratchCardsValue.textContent = currentUser.scratch_cards_left ?? '0';
// Update page-specific displays
spinPageSpinsLeft.textContent = currentUser.spins_left ?? '0';
slotPageTokens.textContent = currentUser.slot_tokens ?? '0';
scratchPageCardsLeft.textContent = currentUser.scratch_cards_left ?? '0';
redeemPointsBalance.textContent = `${currentUser.points?.toLocaleString() ?? '0'} pts`;
profileUsername.textContent = currentUser.username ?? '---';
profilePoints.textContent = `${currentUser.points?.toLocaleString() ?? '0'} pts`;
profileRefCode.textContent = currentUser.referral_code ?? '---';
profileMemberSince.textContent = currentUser.memberSince ?? '---';
profileLastLogin.textContent = currentUser.lastLogin ?? '---';
referralCodeDisplay.textContent = currentUser.referral_code ?? 'N/A';
// Update buttons based on state
spinNowBtn.disabled = isSpinningWheel || (currentUser.spins_left <= 0) || (spinCooldown > 0);
playSlotsBtn.disabled = isSpinningSlots || (currentUser.slot_tokens <= 0);
scratchNowBtn.disabled = isScratching || (currentUser.scratch_cards_left <= 0);
updateDailyBonusButton();
// Update activity list on dashboard
renderActivityLog(dashboardActivityList, MAX_DASHBOARD_ACTIVITIES, currentUser.activityLog || []);
// If user just logged in, navigate to dashboard
if (currentPage === 'login-content' || currentPage === 'register-content') {
showPage('dashboard-content');
}
} else {
// Logged out state
body.classList.remove('logged-in');
body.classList.add('logged-out');
userGreeting.textContent = 'Hi, Guest!';
currentUser = null;
// Reset displays to placeholders
dashboardUsername.textContent = "User";
balanceValue.textContent = "--- pts";
spinsLeftValue.textContent = "--";
slotTokensValue.textContent = "--";
scratchCardsValue.textContent = "--";
spinPageSpinsLeft.textContent = "--";
slotPageTokens.textContent = "--";
scratchPageCardsLeft.textContent = "--";
redeemPointsBalance.textContent = "--- pts";
profileUsername.textContent = "---";
profilePoints.textContent = "---";
profileRefCode.textContent = "---";
profileMemberSince.textContent = "---";
profileLastLogin.textContent = "---";
referralCodeDisplay.textContent = "LOGIN TO SEE";
dashboardActivityList.innerHTML = '<p>Please log in to see activity.</p>';
fullActivityList.innerHTML = '<p>Please log in to see history.</p>';
// Reset game states/buttons
spinNowBtn.disabled = true;
playSlotsBtn.disabled = true;
scratchNowBtn.disabled = true;
claimBonusBtn.disabled = true;
resetScratchCardVisuals();
// If logged out unexpectedly, show login page
if (currentPage !== 'login-content' && currentPage !== 'register-content') {
showPage('login-content');
}
}
showPage(currentPage);
}
function showPage(pageId) {
if (!pageId) return;
const cleanPageId = pageId.replace(/^#/, '');
const targetPage = document.getElementById(cleanPageId);
if (!targetPage) {
console.warn(`Page with ID "${cleanPageId}" not found. Defaulting.`);
pageId = isLoggedIn ? 'dashboard-content' : 'login-content';
} else {
pageId = cleanPageId;
}
pageContents.forEach(page => page.classList.remove('active-page'));
document.getElementById(pageId)?.classList.add('active-page');
currentPage = pageId;
sidebarLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('data-page') === pageId || link.hash === `#${pageId}`) {
link.classList.add('active');
}
});
if (window.innerWidth < 992) closeSidebar();
if (isLoggedIn) {
switch (pageId) {
case 'dashboard-content':
break;
case 'history-content':
loadActivityHistory();
break;
case 'daily-bonus-content':
updateDailyBonusButton();
break;
case 'spin-wheel-content':
resetSpinWheelVisuals();
break;
case 'scratch-cards-content':
resetScratchCardVisuals();
break;
}
} else {
if (pageId === 'spin-wheel-content') resetSpinWheelVisuals();
if (pageId === 'scratch-cards-content') resetScratchCardVisuals();
}
}
function toggleSidebar() {
sidebar.classList.toggle('open');
sidebarOverlay.classList.toggle('show');
}
function closeSidebar() {
sidebar.classList.remove('open');
sidebarOverlay.classList.remove('show');
}
function showLoading(button, show = true) {
if (!button) return;
if (show) {
button.classList.add('loading');
button.disabled = true;
} else {
button.classList.remove('loading');
button.disabled = false;
}
}
function displayMessage(element, message, type = 'error', timeout = 5000) {
if (!element) return;
const messageText = (typeof message === 'string') ? message : 'An unexpected error occurred.';
element.innerHTML = `<div class="${type}-message">${messageText}</div>`;
if (timeout > 0) {
setTimeout(() => {
const currentMessageDiv = element.querySelector(`.${type}-message`);
if (currentMessageDiv && currentMessageDiv.textContent === messageText) {
element.innerHTML = '';
}
}, timeout);
}
}
function clearMessage(element) {
if (element) element.innerHTML = '';
}
// --- Authentication Logic ---
async function handleLogin(event) {
event.preventDefault();
if (isProcessingAuth) return;
const username = loginUsernameInput.value.trim();
const password = loginPasswordInput.value.trim();
clearMessage(loginErrorMessage);
if (!username || !password) {
displayMessage(loginErrorMessage, 'Please enter both username and password.');
return;
}
isProcessingAuth = true;
showLoading(loginButton);
const response = await apiCall('login', { username, password });
if (response.success && response.user) {
isLoggedIn = true;
currentUser = response.user;
updateUI();
loginForm.reset();
} else {
displayMessage(loginErrorMessage, response.message || 'Login failed.');
isLoggedIn = false;
currentUser = null;
updateUI();
}
showLoading(loginButton, false);
isProcessingAuth = false;
}
async function handleRegister(event) {
event.preventDefault();
if (isProcessingAuth) return;
const username = registerUsernameInput.value.trim();
const password = registerPasswordInput.value.trim();
const confirmPassword = registerConfirmPasswordInput.value.trim();
clearMessage(registerErrorMessage);
clearMessage(registerSuccessMessage);
if (!username || !password || !confirmPassword) {
displayMessage(registerErrorMessage, 'Please fill in all fields.'); return;
}
if (!/^[a-zA-Z0-9_]{4,}$/.test(username)) {
displayMessage(registerErrorMessage, 'Username must be 4+ characters (letters, numbers, underscores only).'); return;
}
if (password.length < 6) {
displayMessage(registerErrorMessage, 'Password must be at least 6 characters long.'); return;
}
if (password !== confirmPassword) {
displayMessage(registerErrorMessage, 'Passwords do not match.'); return;
}
isProcessingAuth = true;
showLoading(registerButton);
const response = await apiCall('register', { username, password });
if (response.success) {
displayMessage(registerSuccessMessage, response.message || 'Registration successful! Please log in.', 'success', 0);
registerForm.reset();
setTimeout(() => {
if (registerSuccessMessage.innerHTML.includes('successful')) {
showPage('login-content');
clearMessage(registerSuccessMessage);
}
}, 2000);
} else {
displayMessage(registerErrorMessage, response.message || 'Registration failed.');
}
showLoading(registerButton, false);
isProcessingAuth = false;
}
async function handleLogout() {
const response = await apiCall('logout', {});
isLoggedIn = false;
currentUser = null;
updateUI();
clearMessage(loginErrorMessage);
clearMessage(registerSuccessMessage);
}
// --- Activity Log ---
function renderActivityLog(listElement, limit = 0, activities = []) {
if (!listElement) return;
listElement.innerHTML = '';
const activitiesToRender = limit > 0 ? activities.slice(0, limit) : activities;
if (activitiesToRender.length === 0) {
listElement.innerHTML = '<p>No activity to display.</p>';
return;
}
activitiesToRender.forEach(activity => {
const item = document.createElement('div');
item.classList.add('activity-item');
const typeClass = activity.activity_type?.split('_')[0] || 'default';
item.setAttribute('data-type', typeClass);
const iconClass = getActivityIconClass(activity.activity_type);
item.innerHTML = `
<div class="icon-wrapper"><i class="fas ${iconClass}"></i></div>
<div class="description">${activity.description || 'Unknown Action'}</div>
<div class="timestamp">${timeAgo(activity.timestamp)}</div>
`;
listElement.appendChild(item);
});
}
function getActivityIconClass(type) {
if (!type) return 'fa-question-circle';
if (type.startsWith('spin')) return 'fa-bullseye';
if (type.startsWith('slots')) return 'fa-dice-three';
if (type.startsWith('scratch')) return 'fa-clone';
if (type === 'daily_bonus') return 'fa-calendar-check';
if (type === 'redeem_request') return 'fa-gift';
if (type === 'login') return 'fa-sign-in-alt';
if (type === 'register') return 'fa-user-plus';
if (type === 'logout') return 'fa-sign-out-alt';
if (type === 'daily_reset') return 'fa-history';
return 'fa-info-circle';
}
function timeAgo(timestamp) {
if (!timestamp) return 'sometime';
try {
const date = new Date(timestamp.replace(' ', 'T') + 'Z');
const now = new Date();
const secondsPast = Math.floor((now.getTime() - date.getTime()) / 1000);
if (isNaN(secondsPast) || secondsPast < 0) return 'just now';
if (secondsPast < 60) return `${secondsPast}s ago`;
if (secondsPast < 3600) return `${Math.floor(secondsPast / 60)}m ago`;
if (secondsPast <= 86400) return `${Math.floor(secondsPast / 3600)}h ago`;
const daysPast = Math.floor(secondsPast / 86400);
if (daysPast < 7) return `${daysPast}d ago`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
} catch (e) {
console.error("Error parsing timestamp:", timestamp, e);
return 'sometime';
}
}
async function loadActivityHistory() {
if (!isLoggedIn) {
fullActivityList.innerHTML = '<p>Please log in to view history.</p>';
return;
}
fullActivityList.innerHTML = '<p><i class="fas fa-spinner fa-spin"></i> Loading history...</p>';
const response = await apiCall('get_activity', { limit: 100 });
if (response.success && response.activities) {
renderActivityLog(fullActivityList, 0, response.activities);
} else {
fullActivityList.innerHTML = '<p>Could not load activity history.</p>';
console.error("Failed to load history:", response.message);
}
}
// --- Game Logic ---
function createWheelSegmentsVisuals() {
spinWheelElement.innerHTML = '';
let gradientString = 'conic-gradient(';
let currentAngle = 0;
spinWheelSegments.forEach((segment, index) => {
const startAngle = currentAngle;
const endAngle = currentAngle + segmentAngle;
gradientString += `${segment.color} ${startAngle}deg ${endAngle}deg`;
if (index < numSegments - 1) gradientString += ', ';
const labelAngleDeg = startAngle + (segmentAngle / 2);
const labelAngleRad = labelAngleDeg * Math.PI / 180;
const labelRadius = spinWheelElement.offsetWidth / 2 * 0.75;
const labelX = (spinWheelElement.offsetWidth / 2) + labelRadius * Math.sin(labelAngleRad);
const labelY = (spinWheelElement.offsetHeight / 2) - labelRadius * Math.cos(labelAngleRad);
const label = document.createElement('div');
label.classList.add('wheel-label');
label.style.position = 'absolute';
label.style.left = `${labelX}px`;
label.style.top = `${labelY}px`;
label.style.transform = `translate(-50%, -50%) rotate(${labelAngleDeg + 90}deg)`;
label.textContent = segment.value > 0 ? segment.value : 'Lose';
spinWheelElement.appendChild(label);
currentAngle = endAngle;
});
gradientString += ')';
spinWheelElement.style.background = gradientString;
}
function resetSpinWheelVisuals() {
spinWheelElement.style.transition = 'none';
spinWheelElement.style.transform = `rotate(${currentWheelRotation % 360}deg)`;
spinResult.textContent = isLoggedIn ? 'Click "Spin Now" to play!' : 'Log in to play!';
}
function startSpinCooldown(seconds) {
spinCooldown = seconds;
const timerElement = document.getElementById('spin-timer');
const spinLink = document.getElementById('spin-link');
spinLink.classList.add('disabled');
timerElement.textContent = `${spinCooldown}s`;
if (spinTimerInterval) clearInterval(spinTimerInterval);
spinTimerInterval = setInterval(() => {
spinCooldown--;
timerElement.textContent = `${spinCooldown}s`;
if (spinCooldown <= 0) {
clearInterval(spinTimerInterval);
spinLink.classList.remove('disabled');
timerElement.textContent = 'Ready!';
updateUI(); // Enable spin button if cooldown is over
}
}, 1000);
}
async function handleSpin() {
if (isSpinningWheel || !currentUser || currentUser.spins_left <= 0 || spinCooldown > 0) {
spinResult.textContent = spinCooldown > 0 ?
"Please wait for cooldown!" :
(currentUser?.spins_left <= 0 ? "No spins left!" : "Already spinning...");
return;
}
isSpinningWheel = true;
showLoading(spinNowBtn);
spinResult.textContent = 'Spinning...';
const response = await apiCall('spin', {});
if (response.success) {
const prize = response.prize;
const winningSegmentIndex = spinWheelSegments.findIndex(s => s.value === prize);
const targetSegmentCenterAngle = (winningSegmentIndex * segmentAngle) + (segmentAngle / 2);
const targetRotation = -targetSegmentCenterAngle;
const fullRotations = 5 + Math.floor(Math.random() * 5);
const finalRotation = currentWheelRotation + (360 * fullRotations) + (targetRotation - (currentWheelRotation % 360));
spinWheelElement.style.transition = `transform 5s cubic-bezier(0.25, 0.1, 0.25, 1)`;
spinWheelElement.style.transform = `rotate(${finalRotation}deg)`;
currentWheelRotation = finalRotation;
setTimeout(() => {
currentUser = response.user;
updateUI();
if (prize > 0) {
spinResult.textContent = `🎉 You won ${prize} points! 🎉`;
} else {
spinResult.textContent = '😕 Better luck next time!';
}
isSpinningWheel = false;
showLoading(spinNowBtn, false);
// Start 15 second cooldown after spin
startSpinCooldown(15);
setTimeout(() => {
spinWheelElement.style.transition = 'transform 0.5s ease-out';
}, 200);
}, 5100);
} else {
spinResult.textContent = `Spin failed: ${response.message}`;
isSpinningWheel = false;
showLoading(spinNowBtn, false);
updateUI();
}
}
// Slot Machine Functions
function buildReelSymbols(reelElement) {
reelElement.innerHTML = '';
const fragment = document.createDocumentFragment();
for (let i = 0; i < numSymbolsPerReel; i++) {
const symbolIndex = Math.floor(Math.random() * slotSymbols.length);
const symbolDiv = document.createElement('div');
symbolDiv.classList.add('reel-symbol');
symbolDiv.textContent = slotSymbols[symbolIndex];
fragment.appendChild(symbolDiv);
}
reelElement.appendChild(fragment);
const topCloneFragment = document.createDocumentFragment();
for (let i = numSymbolsPerReel - 3; i < numSymbolsPerReel; i++) {
topCloneFragment.appendChild(reelElement.children[i % reelElement.children.length].cloneNode(true));
}
reelElement.prepend(topCloneFragment);
const bottomCloneFragment = document.createDocumentFragment();
for (let i = 0; i < 3; i++) {
bottomCloneFragment.appendChild(reelElement.children[i + 3].cloneNode(true));
}
reelElement.appendChild(bottomCloneFragment);
reelElement.style.transition = 'none';
const initialOffset = -(3 * reelHeight);
reelElement.style.transform = `translateY(${initialOffset}px)`;
}
function spinReelToSymbol(reelSymbolsElement, targetSymbol, reelIndex) {
return new Promise(resolve => {
const children = reelSymbolsElement.children;
let targetIndex = -1;
for (let i = 3; i < numSymbolsPerReel + 3; i++) {
if (children[i].textContent === targetSymbol) {
targetIndex = i;
break;
}
}
if (targetIndex === -1) targetIndex = 3 + Math.floor(Math.random() * numSymbolsPerReel);
const totalSymbolCount = children.length;
const singleSymbolHeight = reelHeight;
const middleRowOffset = singleSymbolHeight;
const targetPosition = -(targetIndex * singleSymbolHeight) + middleRowOffset;
const spinCycles = 3 + Math.random() * 3 + reelIndex * 0.5;
const fullStripHeight = numSymbolsPerReel * singleSymbolHeight;
const extraSpinHeight = fullStripHeight * spinCycles;
const finalPosition = targetPosition - extraSpinHeight;
const duration = 2.5 + Math.random() * 1.0 + reelIndex * 0.2;
reelSymbolsElement.style.transition = `transform ${duration}s cubic-bezier(0.33, 1, 0.68, 1)`;
reelSymbolsElement.style.transform = `translateY(${finalPosition}px)`;
setTimeout(() => {
reelSymbolsElement.style.transition = 'none';
reelSymbolsElement.style.transform = `translateY(${targetPosition}px)`;
resolve();
}, duration * 1000 + 50);
});
}
async function handleSlots() {
if (isSpinningSlots || !currentUser || currentUser.slot_tokens <= 0) {
slotsResult.textContent = currentUser?.slot_tokens <= 0 ? "No tokens left!" : "Reels already spinning...";
return;
}
isSpinningSlots = true;
showLoading(playSlotsBtn);
slotsResult.textContent = 'Spinning reels...';
const response = await apiCall('play_slots', {});
if (response.success) {
const resultReels = response.reels;
const prize = response.prize;
const win = response.win;
const reelElements = [reel1Symbols, reel2Symbols, reel3Symbols];
const spinPromises = resultReels.map((symbol, index) =>
spinReelToSymbol(reelElements[index], symbol, index)
);
Promise.all(spinPromises).then(() => {
currentUser = response.user;
updateUI();
if (win) {
slotsResult.textContent = `🎉 Match! You won ${prize} points! 🎉 (${resultReels.join(' ')})`;
} else {
slotsResult.textContent = `😕 No win this time. Try again! (${resultReels.join(' ')})`;
}
isSpinningSlots = false;
showLoading(playSlotsBtn, false);
updateUI();
});
} else {
slotsResult.textContent = `Play failed: ${response.message}`;
isSpinningSlots = false;
showLoading(playSlotsBtn, false);
if (isLoggedIn) await refreshUserData(); else updateUI();
}
}
// Scratch Card Functions
function resetScratchCardVisuals() {
scratchCardOverlay.classList.remove('scratched');
scratchCardResult.classList.remove('revealed');
scratchCardResult.textContent = '? Pts';
scratchStatus.textContent = isLoggedIn ? 'Click the card or button below to scratch!' : 'Log in to play!';
scratchCardArea.style.cursor = 'pointer';
}
function handleScratchInteraction(event) {
if (event.target === scratchCardOverlay || event.target.closest('.scratch-overlay') || event.target === scratchNowBtn || event.target.closest('#scratch-now-btn')) {
handleScratch();
}
}
async function handleScratch() {
if (isScratching || !currentUser || currentUser.scratch_cards_left <= 0) {
scratchStatus.textContent = currentUser?.scratch_cards_left <= 0 ? "No cards left today!" : "Already scratching...";
return;
}
isScratching = true;
showLoading(scratchNowBtn);
scratchStatus.textContent = 'Scratching...';
scratchCardArea.style.cursor = 'default';
const response = await apiCall('scratch', {});
if (response.success) {
const prize = response.prize;
scratchCardOverlay.classList.add('scratched');
setTimeout(() => {
scratchCardResult.textContent = prize > 0 ? `🎉 ${prize} Pts! 🎉` : '😕 No Win';
scratchCardResult.classList.add('revealed');
currentUser = response.user;
updateUI();
scratchStatus.textContent = prize > 0 ? `You won ${prize} points!` : "Better luck next time!";
isScratching = false;
showLoading(scratchNowBtn, false);
updateUI();
}, 800);
} else {
scratchStatus.textContent = `Scratch failed: ${response.message}`;
isScratching = false;
showLoading(scratchNowBtn, false);
scratchCardArea.style.cursor = 'pointer';
if (isLoggedIn) await refreshUserData(); else updateUI();
}
}
// --- Other Features Logic ---
function updateDailyBonusButton() {
if (!currentUser || !claimBonusBtn) return;
const today = new Date().toISOString().slice(0, 10);
if (currentUser.last_daily_bonus_claim === today) {
claimBonusBtn.disabled = true;
claimBonusBtn.querySelector('span:not(.spinner)').textContent = 'Bonus Claimed Today';
dailyBonusMessage.textContent = "You've already claimed your bonus for today. Come back tomorrow!";
clearMessage(dailyBonusStatus);
} else {
claimBonusBtn.disabled = isClaimingBonus;
claimBonusBtn.querySelector('span:not(.spinner)').textContent = 'Claim Today\'s Bonus';
dailyBonusMessage.textContent = "Check back every day to claim your free bonus points!";
clearMessage(dailyBonusStatus);
}
}
async function handleDailyBonus() {
if (isClaimingBonus || !currentUser || claimBonusBtn.disabled) return;
isClaimingBonus = true;
showLoading(claimBonusBtn);
clearMessage(dailyBonusStatus);
const response = await apiCall('claim_bonus', {});
if (response.success) {
currentUser = response.user;
displayMessage(dailyBonusStatus,
`🎁 You claimed ${response.bonusAmount} bonus points!`,
'success',
5000);
updateUI();
} else {
displayMessage(dailyBonusStatus, response.message || 'Failed to claim bonus.', 'error');
updateDailyBonusButton();
}
isClaimingBonus = false;
showLoading(claimBonusBtn, false);
updateDailyBonusButton();
}
function handleCopyCode() {
if (!currentUser || !currentUser.referral_code) return;
navigator.clipboard.writeText(currentUser.referral_code)
.then(() => {
displayMessage(copyStatus, 'Referral code copied!', 'success', 3000);
})
.catch(err => {
console.error('Failed to copy code: ', err);
displayMessage(copyStatus, 'Could not copy code.', 'error', 3000);
});
}
async function handleWithdraw(event) {
event.preventDefault();
if (isWithdrawing || !currentUser) return;
const amount = parseInt(withdrawAmountInput.value, 10);
const method = document.getElementById('withdraw-method').value;
const details = withdrawDetailsInput.value.trim();
clearMessage(withdrawErrorMessage);
clearMessage(withdrawSuccessMessage);
const minWithdrawal = 100;
if (isNaN(amount) || amount < minWithdrawal) {
displayMessage(withdrawErrorMessage, `Minimum withdrawal amount is ${minWithdrawal} points.`);
return;
}
if (amount > currentUser.points) {
displayMessage(withdrawErrorMessage, 'Insufficient points balance.');
return;
}
if (!method) {
displayMessage(withdrawErrorMessage, 'Please select a withdrawal method.');
return;
}
if (!details) {
displayMessage(withdrawErrorMessage, 'Please provide account details.');
return;
}
isWithdrawing = true;
showLoading(withdrawButton);
const response = await apiCall('request_withdrawal', { amount, method, details });
if (response.success) {
currentUser = response.user;
displayMessage(withdrawSuccessMessage,
`Withdrawal request submitted! You will receive payment in 24 hours.`,
'success',
0);
// Show screenshot section
screenshotSection.style.display = 'block';
// Scroll to screenshot section
screenshotSection.scrollIntoView({
behavior: 'smooth'
});
withdrawForm.reset();
document.querySelectorAll('.withdrawal-method').forEach(m => m.classList.remove('selected'));
document.getElementById('withdraw-method').value = '';
updateUI();
} else {
displayMessage(withdrawErrorMessage, response.message || 'Withdrawal request failed.');
}
isWithdrawing = false;
showLoading(withdrawButton, false);
}
async function refreshUserData() {
if (!isLoggedIn) return;
const response = await apiCall('get_user_data', {}, 'GET');
if (response.success && response.user) {
currentUser = response.user;
updateUI();
} else {
console.warn("Failed to refresh user data:", response.message);
if (response.message?.includes('Authentication required')) {
handleLogout();
}
}
}
// --- Initial Setup & Event Listeners ---
function initializeApp() {
createWheelSegmentsVisuals();
buildReelSymbols(reel1Symbols);
buildReelSymbols(reel2Symbols);
buildReelSymbols(reel3Symbols);
updateUI();
// Event Listeners
menuToggle.addEventListener('click', toggleSidebar);
sidebarOverlay.addEventListener('click', closeSidebar);
body.addEventListener('click', (e) => {
const sidebarLink = e.target.closest('.sidebar-link');
if (sidebarLink) {
e.preventDefault();
if (sidebarLink.id === 'logout-link') {
handleLogout();
} else {
const pageId = sidebarLink.getAttribute('data-page');
if (pageId) showPage(pageId);
}
return;
}
const navLink = e.target.closest('.nav-link');
if (navLink) {
const pageId = navLink.getAttribute('data-page');
if (pageId) {
e.preventDefault();
showPage(pageId);
}
}
});
// Withdrawal method selection
document.querySelectorAll('.withdrawal-method').forEach(method => {
method.addEventListener('click', () => {
document.querySelectorAll('.withdrawal-method').forEach(m => m.classList.remove('selected'));
method.classList.add('selected');
document.getElementById('withdraw-method').value = method.dataset.method;
});
});
// Auth forms
if (loginForm) loginForm.addEventListener('submit', handleLogin);
if (registerForm) registerForm.addEventListener('submit', handleRegister);
// Game Buttons
if (spinNowBtn) spinNowBtn.addEventListener('click', handleSpin);
if (playSlotsBtn) playSlotsBtn.addEventListener('click', handleSlots);
if (scratchCardArea) scratchCardArea.addEventListener('click', handleScratchInteraction);
if (scratchNowBtn) scratchNowBtn.addEventListener('click', handleScratchInteraction);
// Other Feature Buttons
if (claimBonusBtn) claimBonusBtn.addEventListener('click', handleDailyBonus);
if (copyCodeBtn) copyCodeBtn.addEventListener('click', handleCopyCode);
if (withdrawForm) withdrawForm.addEventListener('submit', handleWithdraw);
// Check for existing session
apiCall('get_user_data', {}, 'GET').then(response => {
if (response.success && response.user) {
isLoggedIn = true;
currentUser = response.user;
const hashPage = window.location.hash.substring(1);
if (hashPage && document.getElementById(hashPage)) {
currentPage = hashPage;
} else {
currentPage = 'dashboard-content';
}
updateUI();
} else {
isLoggedIn = false;
currentUser = null;
const hashPage = window.location.hash.substring(1);
currentPage = (hashPage === 'register-content') ? 'register-content' : 'login-content';
updateUI();
}
});
// Handle hash changes
window.addEventListener('hashchange', () => {
const hashPage = window.location.hash.substring(1);
if (hashPage) {
const targetPageElement = document.getElementById(hashPage);
const isAuthPage = hashPage === 'login-content' || hashPage === 'register-content';
if (targetPageElement) {
if (isLoggedIn && !isAuthPage) {
showPage(hashPage);
} else if (!isLoggedIn && isAuthPage) {
showPage(hashPage);
} else if (!isLoggedIn && !isAuthPage) {
showPage('login-content');
window.location.hash = '#login';
} else if (isLoggedIn && isAuthPage) {
showPage('dashboard-content');
window.location.hash = '#dashboard';
}
}
} else if(isLoggedIn) {
showPage('dashboard-content');
} else {
showPage('login-content');
}
});
if (window.location.hash) {
window.dispatchEvent(new HashChangeEvent('hashchange'));
}
}
initializeApp();
});
</script>
</body>
</html>
api.php
<?php
header('Content-Type: application/json');
session_start();
// Enable CORS for development
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
// Database simulation (using session for demo purposes)
if (!isset($_SESSION['users'])) {
$_SESSION['users'] = [];
$_SESSION['activities'] = [];
}
// Helper functions
function generateRandomString($length = 8) {
$characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$result = '';
for ($i = 0; $i < $length; $i++) {
$result .= $characters[rand(0, strlen($characters) - 1)];
}
return $result;
}
function addActivity($username, $activityType, $description) {
$_SESSION['activities'][$username][] = [
'activity_type' => $activityType,
'description' => $description,
'timestamp' => date('Y-m-d H:i:s')
];
}
// Get input data
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? $_GET['action'] ?? '';
$response = ['success' => false, 'message' => 'Invalid action'];
try {
switch ($action) {
case 'login':
$username = $input['username'] ?? '';
$password = $input['password'] ?? '';
if (empty($username) || empty($password)) {
$response['message'] = 'Username and password are required';
break;
}
// In a real app, you would verify against a database with hashed passwords
if (isset($_SESSION['users'][$username]) && $_SESSION['users'][$username]['password'] === $password) {
$_SESSION['logged_in'] = true;
$_SESSION['username'] = $username;
$response = [
'success' => true,
'user' => $_SESSION['users'][$username],
'message' => 'Login successful'
];
addActivity($username, 'login', 'User logged in');
} else {
$response['message'] = 'Invalid username or password';
}
break;
case 'register':
$username = $input['username'] ?? '';
$password = $input['password'] ?? '';
if (empty($username) || empty($password)) {
$response['message'] = 'Username and password are required';
break;
}
if (isset($_SESSION['users'][$username])) {
$response['message'] = 'Username already exists';
break;
}
// Create new user
$_SESSION['users'][$username] = [
'username' => $username,
'password' => $password, // In real app, hash this
'points' => 0,
'spins_left' => 3,
'slot_tokens' => 5,
'scratch_cards_left' => 2,
'referral_code' => generateRandomString(),
'last_daily_bonus_claim' => null,
'memberSince' => date('Y-m-d H:i:s'),
'lastLogin' => date('Y-m-d H:i:s')
];
$response = [
'success' => true,
'message' => 'Registration successful! Please log in.',
'user' => $_SESSION['users'][$username]
];
addActivity($username, 'register', 'User registered');
break;
case 'logout':
session_destroy();
$response = ['success' => true, 'message' => 'Logged out successfully'];
break;
case 'get_user_data':
if (!isset($_SESSION['logged_in']) || !isset($_SESSION['username'])) {
$response['message'] = 'Authentication required';
break;
}
$username = $_SESSION['username'];
$user = $_SESSION['users'][$username] ?? null;
if ($user) {
$response = [
'success' => true,
'user' => $user,
'activityLog' => $_SESSION['activities'][$username] ?? []
];
} else {
$response['message'] = 'User not found';
}
break;
case 'spin':
if (!isset($_SESSION['logged_in']) || !isset($_SESSION['username'])) {
$response['message'] = 'Authentication required';
break;
}
$username = $_SESSION['username'];
$user = &$_SESSION['users'][$username];
if ($user['spins_left'] <= 0) {
$response['message'] = 'No spins left';
break;
}
// Decrement spins
$user['spins_left']--;
// Determine prize (simplified for demo)
$prizeOptions = [0, 0, 5, 10, 15, 25, 50, 75, 100, 200];
$prize = $prizeOptions[array_rand($prizeOptions)];
// Add prize to balance
$user['points'] += $prize;
// Log activity
$activityType = $prize > 0 ? 'spin_win' : 'spin_lose';
addActivity($username, $activityType, $prize > 0 ? "Won {$prize} points on spin wheel" : "No win on spin wheel");
$response = [
'success' => true,
'prize' => $prize,
'user' => $user,
'message' => $prize > 0 ? "You won {$prize} points!" : "No win this time"
];
break;
case 'play_slots':
if (!isset($_SESSION['logged_in']) || !isset($_SESSION['username'])) {
$response['message'] = 'Authentication required';
break;
}
$username = $_SESSION['username'];
$user = &$_SESSION['users'][$username];
if ($user['slot_tokens'] <= 0) {
$response['message'] = 'No tokens left';
break;
}
// Decrement tokens
$user['slot_tokens']--;
// Generate random reels
$symbols = ['🍒', '🍋', '🍊', '🍉', '⭐', '🔔', '7'];
$reels = [
$symbols[array_rand($symbols)],
$symbols[array_rand($symbols)],
$symbols[array_rand($symbols)]
];
// Determine win (simplified logic)
$prize = 0;
if ($reels[0] === $reels[1] && $reels[1] === $reels[2]) {
// All three match
$prize = 100;
} elseif ($reels[0] === $reels[1] || $reels[1] === $reels[2] || $reels[0] === $reels[2]) {
// Two match
$prize = 25;
}
// Add prize to balance
$user['points'] += $prize;
// Log activity
$activityType = $prize > 0 ? 'slots_win' : 'slots_lose';
addActivity($username, $activityType, $prize > 0 ? "Won {$prize} points on slots" : "No win on slots");
$response = [
'success' => true,
'reels' => $reels,
'prize' => $prize,
'win' => $prize > 0,
'user' => $user,
'message' => $prize > 0 ? "You won {$prize} points!" : "No win this time"
];
break;
case 'scratch':
if (!isset($_SESSION['logged_in']) || !isset($_SESSION['username'])) {
$response['message'] = 'Authentication required';
break;
}
$username = $_SESSION['username'];
$user = &$_SESSION['users'][$username];
if ($user['scratch_cards_left'] <= 0) {
$response['message'] = 'No scratch cards left today';
break;
}
// Decrement cards
$user['scratch_cards_left']--;
// Determine prize (higher chance of small wins)
$prizeOptions = [0, 0, 5, 10, 10, 15, 20, 25, 50];
$prize = $prizeOptions[array_rand($prizeOptions)];
// Add prize to balance
$user['points'] += $prize;
// Log activity
$activityType = $prize > 0 ? 'scratch_win' : 'scratch_lose';
addActivity($username, $activityType, $prize > 0 ? "Won {$prize} points on scratch card" : "No win on scratch card");
$response = [
'success' => true,
'prize' => $prize,
'user' => $user,
'message' => $prize > 0 ? "You won {$prize} points!" : "No win this time"
];
break;
case 'claim_bonus':
if (!isset($_SESSION['logged_in']) || !isset($_SESSION['username'])) {
$response['message'] = 'Authentication required';
break;
}
$username = $_SESSION['username'];
$user = &$_SESSION['users'][$username];
$today = date('Y-m-d');
if ($user['last_daily_bonus_claim'] === $today) {
$response['message'] = 'Bonus already claimed today';
break;
}
// Give bonus (random between 10-50)
$bonus = rand(10, 50);
$user['points'] += $bonus;
$user['last_daily_bonus_claim'] = $today;
// Log activity
addActivity($username, 'daily_bonus', "Claimed daily bonus of {$bonus} points");
$response = [
'success' => true,
'bonusAmount' => $bonus,
'user' => $user,
'message' => "You claimed {$bonus} bonus points!"
];
break;
case 'request_withdrawal':
if (!isset($_SESSION['logged_in']) || !isset($_SESSION['username'])) {
$response['message'] = 'Authentication required';
break;
}
$username = $_SESSION['username'];
$user = &$_SESSION['users'][$username];
$amount = intval($input['amount'] ?? 0);
$method = $input['method'] ?? '';
$details = $input['details'] ?? '';
if ($amount < 100) {
$response['message'] = 'Minimum withdrawal is 100 points';
break;
}
if ($amount > $user['points']) {
$response['message'] = 'Insufficient points';
break;
}
if (empty($method)) {
$response['message'] = 'Withdrawal method required';
break;
}
if (empty($details)) {
$response['message'] = 'Withdrawal details required';
break;
}
// Deduct points
$user['points'] -= $amount;
// Log activity
addActivity($username, 'redeem_request', "Requested withdrawal of {$amount} points via {$method}");
$response = [
'success' => true,
'user' => $user,
'message' => "Withdrawal request for {$amount} points submitted (simulation)"
];
break;
case 'get_activity':
if (!isset($_SESSION['logged_in']) || !isset($_SESSION['username'])) {
$response['message'] = 'Authentication required';
break;
}
$username = $_SESSION['username'];
$activities = array_reverse($_SESSION['activities'][$username] ?? []);
$limit = intval($input['limit'] ?? 20);
$response = [
'success' => true,
'activities' => array_slice($activities, 0, $limit)
];
break;
default:
$response['message'] = 'Unknown action';
}
} catch (Exception $e) {
$response['message'] = 'Server error: ' . $e->getMessage();
}
echo json_encode($response);
?>
ads
databas
-- Database: spin_earn
-- Users Table
CREATE TABLE users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(100) UNIQUE,
referral_code VARCHAR(20) UNIQUE NOT NULL,
referred_by INT DEFAULT NULL,
points DECIMAL(10,2) DEFAULT 0.00,
spins_left INT DEFAULT 3,
slot_tokens INT DEFAULT 5,
scratch_cards_left INT DEFAULT 3,
last_daily_bonus_claim DATE DEFAULT NULL,
member_since TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP NULL,
FOREIGN KEY (referred_by) REFERENCES users(user_id)
);
-- Activity Log Table
CREATE TABLE activity_log (
activity_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
activity_type VARCHAR(50) NOT NULL,
description VARCHAR(255) NOT NULL,
points_change DECIMAL(10,2) DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
-- Withdrawal Requests Table
CREATE TABLE withdrawal_requests (
request_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
method ENUM('easypaisa', 'jazzcash') NOT NULL,
account_details VARCHAR(255) NOT NULL,
status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending',
screenshot_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
processed_at TIMESTAMP NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
-- Spin Wheel Prizes Table
CREATE TABLE spin_prizes (
prize_id INT AUTO_INCREMENT PRIMARY KEY,
value DECIMAL(10,2) NOT NULL,
probability DECIMAL(5,2) NOT NULL, -- Percentage chance (0-100)
is_active BOOLEAN DEFAULT TRUE
);
-- Slot Machine Symbols Table
CREATE TABLE slot_symbols (
symbol_id INT AUTO_INCREMENT PRIMARY KEY,
symbol VARCHAR(10) NOT NULL,
weight INT NOT NULL, -- Relative weight for probability
multiplier DECIMAL(3,2) DEFAULT 1.00, -- Multiplier for winnings
is_active BOOLEAN DEFAULT TRUE
);
-- Scratch Card Prizes Table
CREATE TABLE scratch_prizes (
prize_id INT AUTO_INCREMENT PRIMARY KEY,
min_value DECIMAL(10,2) NOT NULL,
max_value DECIMAL(10,2) NOT NULL,
probability DECIMAL(5,2) NOT NULL, -- Percentage chance (0-100)
is_active BOOLEAN DEFAULT TRUE
);
-- Daily Bonus Table
CREATE TABLE daily_bonuses (
bonus_id INT AUTO_INCREMENT PRIMARY KEY,
day_number INT NOT NULL, -- 1 for first day, 2 for second, etc.
min_bonus DECIMAL(10,2) NOT NULL,
max_bonus DECIMAL(10,2) NOT NULL,
streak_multiplier DECIMAL(3,2) DEFAULT 1.00 -- Bonus for consecutive days
);
-- System Settings Table
CREATE TABLE system_settings (
setting_id INT AUTO_INCREMENT PRIMARY KEY,
setting_name VARCHAR(50) UNIQUE NOT NULL,
setting_value VARCHAR(255) NOT NULL,
description VARCHAR(255)
);
-- Insert initial spin prizes
INSERT INTO spin_prizes (value, probability) VALUES
(100.00, 10.00), -- 10% chance
(50.00, 15.00), -- 15% chance
(25.00, 20.00), -- 20% chance
(10.00, 25.00), -- 25% chance
(5.00, 20.00), -- 20% chance
(0.00, 10.00); -- 10% chance (no win)
-- Insert slot symbols
INSERT INTO slot_symbols (symbol, weight, multiplier) VALUES
('🍒', 30, 1.00),
('🍋', 25, 1.20),
('🍊', 20, 1.50),
('🍉', 15, 2.00),
('⭐', 7, 5.00),
('🔔', 3, 10.00),
('7', 1, 20.00);
-- Insert scratch card prizes
INSERT INTO scratch_prizes (min_value, max_value, probability) VALUES
(1.00, 5.00, 50.00), -- 50% chance for small prize
(5.00, 20.00, 30.00), -- 30% chance for medium prize
(20.00, 50.00, 15.00), -- 15% chance for large prize
(50.00, 100.00, 5.00); -- 5% chance for jackpot
-- Insert daily bonuses
INSERT INTO daily_bonuses (day_number, min_bonus, max_bonus, streak_multiplier) VALUES
(1, 10.00, 20.00, 1.00),
(2, 15.00, 25.00, 1.10),
(3, 20.00, 30.00, 1.20),
(4, 25.00, 35.00, 1.30),
(5, 30.00, 40.00, 1.40),
(6, 35.00, 45.00, 1.50),
(7, 50.00, 100.00, 2.00);
-- Insert system settings
INSERT INTO system_settings (setting_name, setting_value, description) VALUES
('daily_spin_reset', '3', 'Number of spins reset daily'),
('daily_slot_tokens', '5', 'Number of slot tokens given daily'),
('daily_scratch_cards', '3', 'Number of scratch cards given daily'),
('min_withdrawal', '100', 'Minimum withdrawal amount'),
('referral_bonus', '50', 'Points given for successful referral'),
('spin_cooldown', '15', 'Cooldown period between spins in seconds');
Comments
Post a Comment