feat: implement advanced reward management and wheel customization

- Add customizable background color
- Add wheel animations and sound effects
- Add reward description field and winner modal
- Change font to Dana
- Add configurable reward probabilities
- Support reward types (Empty, Discount Code, LiveCoin)
- Replace reward title with reward type selector
- Allow wheel page title customization from admin panel
This commit is contained in:
2026-06-10 14:55:07 +03:30
parent b0ff339525
commit ec86cdc528
26 changed files with 1166 additions and 533 deletions

View File

@@ -1,92 +1,161 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>پنل مدیریت</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<body id="main-body">
<div class="admin-container">
<div class="admin-header">
<h1> مدیریت گردونه شانس</h1>
<h1>مدیریت گردونه شانس</h1>
<p>مدیریت آیتم‌ها و جوایز گردونه</p>
</div>
<!-- تنظیمات رنگ پس‌زمینه -->
<div class="card">
<h2>تنظیمات ظاهری</h2>
<div class="color-section">
<label>رنگ پس‌زمینه:</label>
<div class="color-presets">
<div class="preset" style="background:#7B241C" data-color="#7B241C"></div>
<div class="preset" style="background:#1a237e" data-color="#1a237e"></div>
<div class="preset" style="background:#1b5e20" data-color="#1b5e20"></div>
<div class="preset" style="background:#4a148c" data-color="#4a148c"></div>
<div class="preset" style="background:#e65100" data-color="#e65100"></div>
<div class="preset" style="background:#006064" data-color="#006064"></div>
<div class="preset" style="background:#212121" data-color="#212121"></div>
<div class="preset" style="background:#880e4f" data-color="#880e4f"></div>
</div>
<div class="color-custom">
<label>رنگ دلخواه:</label>
<input type="color" id="bg-color-picker" value="#7B241C">
<button id="apply-color">اعمال</button>
</div>
<div class="color-preview">
<span>پیش‌نمایش:</span>
<div id="color-preview-box"></div>
</div>
</div>
</div>
<!-- عنوان صفحه گردونه -->
<div class="title-section">
<h3>عنوان صفحه گردونه</h3>
<div class="form-grid">
<div class="form-group">
<label>متن عنوان:</label>
<input type="text" id="title-text-input" placeholder="گردونه شانس">
</div>
<div class="form-group">
<label>رنگ عنوان:</label>
<div style="display:flex; align-items:center; gap:10px;">
<input type="color" id="title-color-picker" value="#FFD700">
<span id="title-preview" style="font-size:20px; font-weight:bold; color:#FFD700;">گردونه شانس</span>
</div>
</div>
<div class="form-group full-width">
<button id="apply-title" class="btn-primary">اعمال</button>
</div>
</div>
</div>
<!-- افزودن آیتم -->
<div class="add-section">
<input
type="text"
id="title-input"
placeholder="عنوان جایزه"
>
<input
type="number"
id="value-input"
placeholder="مقدار جایزه"
>
<button id="add-btn">
افزودن
</button>
<div class="card">
<h2>افزودن آیتم جدید</h2>
<div class="form-grid">
<div class="form-group">
<label>نوع جایزه:</label>
<select id="new-prize-type">
<option value="پوچ">پوچ</option>
<option value="کد تخفیف">کد تخفیف</option>
<option value="لایوکوین">لایوکوین (LC)</option>
</select>
</div>
<div class="form-group">
<label>عنوان:</label>
<input type="text" id="new-title" placeholder="مثلاً: تخفیف ۲۰٪">
</div>
<div class="form-group">
<label>مقدار جایزه:</label>
<input type="text" id="new-prize" placeholder="مثلاً: DISC20">
</div>
<div class="form-group">
<label>احتمال (۱ = عادی):</label>
<input type="number" id="new-probability" value="1" min="0.1" step="0.1">
</div>
<div class="form-group full-width">
<label>توضیحات:</label>
<textarea id="new-description" placeholder="توضیحاتی که بعد از برنده شدن نمایش داده میشه..."></textarea>
</div>
<div class="form-group full-width">
<button id="add-btn" class="btn-primary">افزودن آیتم</button>
</div>
</div>
</div>
<!-- جدول -->
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>#</th>
<th>عنوان جایزه</th>
<th>مقدار</th>
<th>عملیات</th>
</tr>
</thead>
<tbody id="items-table-body">
<!-- ردیف نمونه (دینامیک توسط JS ساخته می‌شود) -->
<!--
<tr>
<td>1</td>
<td>جایزه تست</td>
<td>100</td>
<td class="actions">
<button class="edit-btn" data-id="1">
✏️ ویرایش
</button>
<button class="delete-btn" data-id="1">
🗑 حذف
</button>
</td>
</tr>
-->
</tbody>
</table>
<div class="card">
<h2>لیست آیتم‌ها</h2>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>#</th>
<th>نوع جایزه</th>
<th>عنوان</th>
<th>مقدار</th>
<th>احتمال</th>
<th>توضیحات</th>
<th>عملیات</th>
</tr>
</thead>
<tbody id="items-table-body"></tbody>
</table>
</div>
</div>
</div>
<!-- مودال ویرایش -->
<div id="edit-modal" class="modal hidden">
<div class="modal-content admin-modal">
<h2>ویرایش آیتم</h2>
<input type="hidden" id="edit-id">
<div class="form-grid">
<div class="form-group">
<label>نوع جایزه:</label>
<select id="edit-prize-type">
<option value="پوچ">پوچ</option>
<option value="کد تخفیف">کد تخفیف</option>
<option value="لایوکوین">لایوکوین (LC)</option>
</select>
</div>
<div class="form-group">
<label>عنوان:</label>
<input type="text" id="edit-title">
</div>
<div class="form-group">
<label>مقدار جایزه:</label>
<input type="text" id="edit-prize">
</div>
<div class="form-group">
<label>احتمال:</label>
<input type="number" id="edit-probability" min="0.1" step="0.1">
</div>
<div class="form-group full-width">
<label>توضیحات:</label>
<textarea id="edit-description"></textarea>
</div>
</div>
<div class="modal-actions">
<button id="save-edit" class="btn-primary">ذخیره</button>
<button id="cancel-edit" class="btn-secondary">انصراف</button>
</div>
</div>
</div>
<script type="module" src="assets/js/admin.js"></script>
</body>
</html>
</html>

View File

@@ -1,261 +1,68 @@
/* =========================
ADMIN PANEL
========================= */
.admin-container {
width: 100%;
max-width: 1200px;
padding: 30px;
@font-face {
font-family: 'Dana';
src: url('../fonts/DanaFaNum-Regular.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
}
.admin-header {
text-align: center;
margin-bottom: 30px;
}
.admin-header h1 {
color: #FFD700;
font-size: 38px;
text-shadow: 0 0 15px rgba(255,215,0,0.5);
}
.admin-header p {
color: #f5d6a0;
margin-top: 10px;
font-size: 18px;
}
.add-section {
background: rgba(0,0,0,0.25);
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 20px;
padding: 20px;
display: flex;
gap: 15px;
margin-bottom: 30px;
box-shadow:
0 10px 30px rgba(0,0,0,0.4),
inset 0 0 20px rgba(255,255,255,0.03);
}
.add-section input {
flex: 1;
padding: 14px;
border: none;
border-radius: 12px;
background: rgba(255,255,255,0.08);
color: white;
font-size: 16px;
outline: none;
}
.add-section input::placeholder {
color: rgba(255,255,255,0.5);
}
#add-btn {
padding: 14px 28px;
background: linear-gradient(
135deg,
#FFD700,
#D4AC0D
);
color: #4a0f0f;
border: none;
border-radius: 14px;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
box-shadow:
0 4px 20px rgba(255,215,0,0.3);
}
#add-btn:hover {
transform: translateY(-2px);
box-shadow:
0 8px 25px rgba(255,215,0,0.45);
}
.table-wrapper {
background: rgba(0,0,0,0.25);
backdrop-filter: blur(10px);
border-radius: 22px;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.08);
box-shadow:
0 10px 40px rgba(0,0,0,0.45);
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background:
linear-gradient(
135deg,
#FFD700,
#D4AC0D
);
color: #4a0f0f;
}
th {
padding: 18px;
font-size: 17px;
}
td {
padding: 18px;
text-align: center;
color: white;
}
tbody tr {
border-bottom:
1px solid rgba(255,255,255,0.06);
transition: 0.2s;
}
tbody tr:hover {
background:
rgba(255,255,255,0.05);
}
.action-buttons {
display: flex;
justify-content: center;
gap: 10px;
}
.edit-btn,
.delete-btn {
border: none;
padding: 10px 16px;
border-radius: 10px;
cursor: pointer;
font-weight: bold;
transition: 0.2s;
}
.edit-btn {
background:
linear-gradient(
135deg,
#9B59B6,
#7D3C98
);
color: white;
}
.delete-btn {
background:
linear-gradient(
135deg,
#E74C3C,
#922B21
);
color: white;
}
.edit-btn:hover,
.delete-btn:hover {
transform: scale(1.05);
}
@media (max-width: 768px) {
.add-section {
flex-direction: column;
}
table {
font-size: 14px;
}
th,
td {
padding: 12px;
}
}
/* =========================
WHEEL PAGE
========================= */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Dana' !important;
}
/* =========================
WHEEL PAGE
========================= */
body {
min-height: 100vh;
background: linear-gradient(135deg, #8B0000 0%, #4a0f0f 50%, #2d0000 100%);
background: radial-gradient(ellipse at center, #9B241CDD 0%, #7B241C 55%, #7B241CBB 100%);
display: flex;
justify-content: center;
align-items: center;
font-family: Tahoma, sans-serif;
direction: rtl;
}
.bg-rays {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200vw;
height: 200vh;
background: repeating-conic-gradient(
rgba(255,255,255,0.03) 0deg,
rgba(255,255,255,0.03) 10deg,
transparent 10deg,
transparent 20deg
);
pointer-events: none;
z-index: 0;
}
.page {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
gap: 28px;
padding: 30px;
}
.page h1 {
color: #FFD700;
font-size: 36px;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
color: #e9e3c0;
font-size: 42px;
font-weight: bold;
text-shadow: 0 0 20px rgba(255,215,0,0.5);
letter-spacing: 2px;
}
.wheel-wrapper {
position: relative;
width: 500px;
height: 500px;
width: 520px;
height: 520px;
}
.wheel-wrapper canvas {
@@ -265,22 +72,21 @@ body {
}
#spin-btn {
padding: 16px 48px;
font-size: 20px;
padding: 18px 56px;
font-size: 24px;
font-weight: bold;
font-family: Tahoma, sans-serif;
background: linear-gradient(135deg, #FFD700, #D4AC0D);
background: linear-gradient(135deg, #e7d78a, #f2dd9d);
color: #4a0f0f;
border: none;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 6px 25px rgba(255, 215, 0, 0.4);
box-shadow: 0 6px 25px rgba(255,215,0,0.4);
transition: 0.2s;
}
#spin-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(255, 215, 0, 0.55);
box-shadow: 0 10px 30px rgba(255,215,0,0.55);
}
#spin-btn:disabled {
@@ -295,10 +101,10 @@ body {
}
.winner-text {
font-size: 28px;
font-size: 32px;
font-weight: bold;
color: #FFD700;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.7);
text-shadow: 0 0 20px rgba(255,215,0,0.7);
animation: pop 0.4s ease;
}
@@ -308,18 +114,380 @@ body {
100% { transform: scale(1); opacity: 1; }
}
/* =========================
مودال برنده
========================= */
.modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal.hidden { display: none; }
.modal-content {
background: white;
border-radius: 20px;
padding: 44px 36px;
text-align: center;
max-width: 420px;
width: 90%;
animation: popIn 0.4s ease;
direction: rtl;
}
@keyframes popIn {
0% { transform: scale(0.5); opacity: 0; }
70% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
.modal-icon { font-size: 70px; margin-bottom: 12px; }
.modal-content h2 {
color: #7B241C;
font-size: 30px;
font-weight: bold;
margin-bottom: 10px;
}
#modal-prize-type {
color: #888;
font-size: 17px;
margin-bottom: 8px;
}
#modal-prize {
color: #C0392B;
font-size: 26px;
font-weight: bold;
margin-bottom: 14px;
}
#modal-description {
color: #555;
font-size: 17px;
margin-bottom: 28px;
line-height: 2;
}
#modal-close {
padding: 14px 48px;
background: linear-gradient(135deg, #FFD700, #D4AC0D);
color: #4a0f0f;
border: none;
border-radius: 50px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
}
/* =========================
ADMIN PANEL
========================= */
.admin-container {
width: 150%;
max-width: 1100px;
margin: 0 auto;
padding: 30px 20px;
display: flex;
flex-direction: column;
gap: 24px;
}
.admin-header {
text-align: center;
margin-bottom: 20px;
}
.admin-header h1 {
color: #e9e8df;
font-size: 40px;
font-weight: bold;
text-shadow: 0 0 15px rgba(255,215,0,0.5);
}
.admin-header p {
color: #f5d6a0;
margin-top: 8px;
font-size: 18px;
}
.card {
background: rgba(0,0,0,0.25);
backdrop-filter: blur(8px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 20px;
padding: 24px;
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
}
.card h2 {
color: #efede1;
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.color-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.color-section label {
color: #f5d6a0;
font-size: 16px;
}
.color-presets {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.preset {
width: 44px;
height: 44px;
border-radius: 8px;
cursor: pointer;
border: 3px solid transparent;
transition: transform 0.2s, border-color 0.2s;
}
.preset:hover { transform: scale(1.15); }
.preset.active { border-color: #e5e3d5; }
.color-custom {
display: flex;
align-items: center;
gap: 12px;
}
.color-preview {
display: flex;
align-items: center;
gap: 12px;
color: #f5d6a0;
font-size: 16px;
}
#color-preview-box {
width: 120px;
height: 36px;
border-radius: 8px;
border: 2px solid rgba(255,255,255,0.2);
transition: background 0.3s;
}
input[type="color"] {
width: 50px;
height: 38px;
border: none;
border-radius: 6px;
cursor: pointer;
padding: 2px;
background: transparent;
}
#apply-color {
padding: 10px 20px;
background: rgba(255,255,255,0.15);
color: white;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
cursor: pointer;
font-size: 15px;
transition: 0.2s;
}
#apply-color:hover { background: rgba(255,255,255,0.25); }
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group.full-width { grid-column: 1 / -1; }
.form-group label {
font-size: 15px;
color: #f5d6a0;
font-weight: bold;
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 12px 14px;
border: 1px solid rgba(255,255,255,0.15);
border-radius: 8px;
font-size: 15px;
background: rgba(255,255,255,0.08);
color: white;
transition: border-color 0.2s;
}
.form-group input::placeholder,
.form-group textarea::placeholder {
color: rgba(255,255,255,0.4);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #e4e2d7;
}
.form-group select option {
background: #4a0f0f;
color: white;
}
.form-group textarea { height: 90px; resize: vertical; }
.btn-primary {
padding: 13px 32px;
background: linear-gradient(135deg, #e3dfb7, #ebe5be);
color: #4a0f0f;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
}
.btn-primary:hover { transform: translateY(-2px); }
.btn-secondary {
padding: 13px 32px;
background: rgba(255,255,255,0.1);
color: white;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: 0.2s;
}
.btn-secondary:hover { background: rgba(255,255,255,0.2); }
.table-wrapper {
overflow-x: auto;
border-radius: 12px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 15px;
}
thead tr {
background: linear-gradient(135deg, #ebe3b9, #ebe1bc);
color: #0f374a;
}
th, td {
padding: 14px 16px;
text-align: right;
}
tbody tr {
border-bottom: 1px solid rgba(255,255,255,0.06);
color: white;
transition: 0.2s;
}
tbody tr:hover { background: rgba(255,255,255,0.05); }
.badge {
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: bold;
}
.badge-pooch { background: rgba(255,255,255,0.1); color: #ccc; }
.badge-discount { background: rgba(46,125,50,0.3); color: #81C784; }
.badge-coin { background: rgba(245,127,23,0.3); color: #FFD700; }
.edit-btn {
padding: 7px 16px;
background: linear-gradient(135deg, #9B59B6, #7D3C98);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-left: 6px;
transition: 0.2s;
}
.delete-btn {
padding: 7px 16px;
background: linear-gradient(135deg, #E74C3C, #922B21);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: 0.2s;
}
.edit-btn:hover, .delete-btn:hover { transform: scale(1.05); }
.modal-content.admin-modal {
background: #2d0000;
color: white;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
}
.modal-content.admin-modal h2 {
color: #FFD700;
margin-bottom: 20px;
}
.modal-actions {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: flex-end;
}
@media (max-width: 550px) {
.wheel-wrapper {
width: 340px;
height: 340px;
}
.wheel-wrapper { width: 340px; height: 340px; }
.page h1 { font-size: 28px; }
#spin-btn { font-size: 18px; padding: 14px 40px; }
.form-grid { grid-template-columns: 1fr; }
}
.page h1 {
font-size: 26px;
}
.title-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
}
#spin-btn {
font-size: 16px;
padding: 12px 36px;
}
.title-section h3 {
color: #f5d6a0;
font-size: 15px;
margin-bottom: 14px;
font-family: 'Dana', Tahoma, sans-serif;
}

Binary file not shown.

View File

@@ -1,103 +1,158 @@
import {
adminGetItems,
createItem,
deleteItem,
updateItem // 👈 باید اینو هم به api.js اضافه کنی
} from './api.js';
import { adminGetItems, createItem, deleteItem, updateItem, saveBgColor, getBgColor, saveTitleSettings, getTitleSettings } from './api.js';
const tableBody = document.getElementById('items-table-body');
const titleInput = document.getElementById('title-input');
const valueInput = document.getElementById('value-input');
const addBtn = document.getElementById('add-btn');
const editModal = document.getElementById('edit-modal');
const saveEdit = document.getElementById('save-edit');
const cancelEdit = document.getElementById('cancel-edit');
const colorPicker = document.getElementById('bg-color-picker');
const applyColor = document.getElementById('apply-color');
const previewBox = document.getElementById('color-preview-box');
const presets = document.querySelectorAll('.preset');
let editId = null; // 👈 حالت ویرایش
// -------------------- Render --------------------
async function renderItems() {
const items = await adminGetItems();
tableBody.innerHTML = '';
items.forEach((item, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${index + 1}</td>
<td>${item.title}</td>
<td>${item.value}</td>
<td class="actions">
<button
class="edit-btn"
onclick="editItem(${item.id}, '${item.title}', ${item.value})"
>
✏️ ویرایش
</button>
<button
class="delete-btn"
onclick="removeItem(${item.id})"
>
🗑 حذف
</button>
</td>
`;
tableBody.appendChild(row);
// ═══ رنگ پس‌زمینه ═══
function initColor() {
const saved = getBgColor();
colorPicker.value = saved;
previewBox.style.background = saved;
presets.forEach(p => {
if (p.dataset.color === saved) p.classList.add('active');
});
}
// -------------------- Delete --------------------
presets.forEach(p => {
p.addEventListener('click', () => {
presets.forEach(x => x.classList.remove('active'));
p.classList.add('active');
colorPicker.value = p.dataset.color;
previewBox.style.background = p.dataset.color;
});
});
colorPicker.addEventListener('input', () => {
previewBox.style.background = colorPicker.value;
presets.forEach(x => x.classList.remove('active'));
});
applyColor.addEventListener('click', () => {
saveBgColor(colorPicker.value);
applyColor.textContent = '✓ ذخیره شد';
setTimeout(() => applyColor.textContent = 'اعمال', 2000);
});
// ═══ عنوان صفحه گردونه ═══
function initTitle() {
const settings = getTitleSettings();
document.getElementById('title-text-input').value = settings.text;
document.getElementById('title-color-picker').value = settings.color;
document.getElementById('title-preview').textContent = settings.text;
document.getElementById('title-preview').style.color = settings.color;
}
document.getElementById('title-text-input').addEventListener('input', (e) => {
document.getElementById('title-preview').textContent = e.target.value || 'گردونه شانس';
});
document.getElementById('title-color-picker').addEventListener('input', (e) => {
document.getElementById('title-preview').style.color = e.target.value;
});
document.getElementById('apply-title').addEventListener('click', () => {
const text = document.getElementById('title-text-input').value.trim() || 'گردونه شانس';
const color = document.getElementById('title-color-picker').value;
saveTitleSettings(text, color);
document.getElementById('apply-title').textContent = '✓ ذخیره شد';
setTimeout(() => document.getElementById('apply-title').textContent = 'اعمال', 2000);
});
// ═══ badge نوع جایزه ═══
function prizeBadge(type) {
const map = {
'پوچ': ['badge-pooch', 'پوچ'],
'کد تخفیف': ['badge-discount', 'کد تخفیف'],
'لایوکوین': ['badge-coin', 'لایوکوین (LC)'],
};
const [cls, label] = map[type] || ['badge-pooch', type];
return `<span class="badge ${cls}">${label}</span>`;
}
// ═══ رندر جدول ═══
async function renderItems() {
const items = await adminGetItems();
tableBody.innerHTML = '';
items.forEach((item, idx) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${idx + 1}</td>
<td>${prizeBadge(item.prize_type)}</td>
<td>${item.title}</td>
<td>${item.prize}</td>
<td>${item.probability}</td>
<td>${item.description || '—'}</td>
<td>
<button class="edit-btn" onclick="openEdit(${item.id})">✏️ ویرایش</button>
<button class="delete-btn" onclick="removeItem(${item.id})">🗑 حذف</button>
</td>
`;
tableBody.appendChild(row);
});
window._items = items;
}
// ═══ افزودن آیتم ═══
addBtn.onclick = async () => {
const data = {
prize_type: document.getElementById('new-prize-type').value,
title: document.getElementById('new-title').value.trim(),
prize: document.getElementById('new-prize').value.trim(),
probability: parseFloat(document.getElementById('new-probability').value),
description: document.getElementById('new-description').value.trim(),
};
if (!data.title || !data.prize) return alert('عنوان و مقدار جایزه رو پر کن');
await createItem(data);
document.getElementById('new-title').value = '';
document.getElementById('new-prize').value = '';
document.getElementById('new-description').value = '';
document.getElementById('new-probability').value = '1';
renderItems();
};
// ═══ حذف ═══
window.removeItem = async (id) => {
if (!confirm('آیا مطمئنی؟')) return;
await deleteItem(id);
renderItems();
};
// -------------------- Edit --------------------
window.editItem = (id, title, value) => {
titleInput.value = title;
valueInput.value = value;
editId = id;
addBtn.textContent = 'آپدیت ✏️';
// ═══ ویرایش ═══
window.openEdit = (id) => {
const item = window._items.find(i => i.id === id);
if (!item) return;
document.getElementById('edit-id').value = item.id;
document.getElementById('edit-prize-type').value = item.prize_type;
document.getElementById('edit-title').value = item.title;
document.getElementById('edit-prize').value = item.prize;
document.getElementById('edit-probability').value = item.probability;
document.getElementById('edit-description').value = item.description || '';
editModal.classList.remove('hidden');
};
// -------------------- Add / Update --------------------
addBtn.onclick = async () => {
const title = titleInput.value.trim();
const value = valueInput.value.trim();
if (!title || !value) {
alert('همه فیلدها را پر کن');
return;
}
// حالت ویرایش
if (editId) {
await updateItem(editId, {
title,
value: parseInt(value)
});
editId = null;
addBtn.textContent = 'افزودن';
}
// حالت افزودن
else {
await createItem({
title,
value: parseInt(value)
});
}
titleInput.value = '';
valueInput.value = '';
cancelEdit.onclick = () => editModal.classList.add('hidden');
saveEdit.onclick = async () => {
const id = document.getElementById('edit-id').value;
const data = {
prize_type: document.getElementById('edit-prize-type').value,
title: document.getElementById('edit-title').value.trim(),
prize: document.getElementById('edit-prize').value.trim(),
probability: parseFloat(document.getElementById('edit-probability').value),
description: document.getElementById('edit-description').value.trim(),
};
await updateItem(id, data);
editModal.classList.add('hidden');
renderItems();
};
// -------------------- Init --------------------
initColor();
initTitle();
renderItems();

View File

@@ -1,40 +1,74 @@
const BASE = 'http://127.0.0.1:8000';
// ---------------- USER ----------------
export const getItems = () =>
fetch(`${BASE}/user/items`)
.then(r => r.json());
fetch(`${BASE}/user/items`).then(r => r.json());
export const spinWheel = () =>
fetch(`${BASE}/user/spin`, {
method: 'POST'
}).then(r => r.json());
fetch(`${BASE}/user/spin`, { method: 'POST' }).then(async (r) => {
if (!r.ok) throw new Error('Spin failed');
return r.json();
});
// ---------------- ADMIN ----------------
export const adminGetItems = () =>
fetch(`${BASE}/admin/items`)
.then(r => r.json());
fetch(`${BASE}/admin/items`).then(r => r.json());
export const createItem = (data) =>
fetch(`${BASE}/admin/items`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: data.title,
prize: data.prize,
prize_type: data.prize_type,
description: data.description,
probability: data.probability
})
}).then(r => r.json());
export const updateItem = (id, data) =>
fetch(`${BASE}/admin/items/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: data.title,
prize: data.prize,
prize_type: data.prize_type,
description: data.description,
probability: data.probability
})
}).then(r => r.json());
export const deleteItem = (id) =>
fetch(`${BASE}/admin/items/${id}`, {
method: 'DELETE'
}).then(r => {
if (r.status === 204) return {};
return r.json();
});
fetch(`${BASE}/admin/items/${id}`, { method: 'DELETE' }).then(async (r) => {
if (!r.ok && r.status !== 204) throw new Error('Delete failed');
return {};
});
// ---------------- رنگ پس‌زمینه ----------------
export const getBgColor = () =>
localStorage.getItem('wheelBgColor') || '#7B241C';
export const saveBgColor = (color) => {
localStorage.setItem('wheelBgColor', color);
window.dispatchEvent(new StorageEvent('storage', {
key: 'wheelBgColor',
newValue: color
}));
};
// ---------------- عنوان صفحه گردونه ----------------
export const getTitleSettings = () => {
const saved = localStorage.getItem('wheelTitleSettings');
return saved ? JSON.parse(saved) : { text: 'گردونه شانس', color: '#FFD700' };
};
export const saveTitleSettings = (text, color) => {
const settings = { text, color };
localStorage.setItem('wheelTitleSettings', JSON.stringify(settings));
window.dispatchEvent(new StorageEvent('storage', {
key: 'wheelTitleSettings',
newValue: JSON.stringify(settings)
}));
};

View File

@@ -1,4 +1,4 @@
import { getItems, spinWheel } from './api.js';
import { getItems, spinWheel, getBgColor, getTitleSettings } from './api.js';
const canvas = document.getElementById('wheel');
const ctx = canvas.getContext('2d');
@@ -8,12 +8,43 @@ const result = document.getElementById('result');
const DARK = { edge: '#6D0025', main: '#cb183d', highlight: '#E8005E', text: '#fff' };
const LIGHT = { edge: '#D4889A', main: '#FAD4DC', highlight: '#FFF0F3', text: '#7B0030' };
let audioCtx = null;
function getAudioCtx() {
if (!audioCtx || audioCtx.state === 'closed') {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
}
let items = [];
let angle = 0;
let spinning = false;
let lampOn = true;
function applyBg() {
const color = getBgColor();
document.getElementById('main-body').style.background =
`radial-gradient(ellipse at center, ${color}DD 0%, ${color} 55%, ${color}BB 100%)`;
}
function applyTitle() {
const settings = getTitleSettings();
const h1 = document.querySelector('.page h1');
if (h1) {
h1.textContent = settings.text;
h1.style.color = settings.color;
h1.style.textShadow = `0 0 20px ${settings.color}80`;
}
}
async function init() {
applyBg();
applyTitle();
window.addEventListener('storage', (e) => {
if (e.key === 'wheelBgColor') applyBg();
if (e.key === 'wheelTitleSettings') applyTitle();
});
items = await getItems();
draw(angle);
setInterval(() => {
@@ -75,7 +106,6 @@ function draw(rotation) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// ═══ سایه کلی ═══
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 30;
@@ -87,7 +117,6 @@ function draw(rotation) {
ctx.fill();
ctx.restore();
// ═══ حلقه بیرونی تیره ═══
const rimGrad = ctx.createRadialGradient(cx - 28, cy - 28, r, cx, cy, r + 40);
rimGrad.addColorStop(0, '#A0003A');
rimGrad.addColorStop(0.3, '#6D0025');
@@ -98,14 +127,12 @@ function draw(rotation) {
ctx.fillStyle = rimGrad;
ctx.fill();
// ═══ بخش‌های گردونه ═══
items.forEach((item, i) => {
const start = rotation + i * slice;
const end = start + slice;
drawSegment(cx, cy, r, start, end, getTheme(i, total));
});
// ═══ خطوط طلایی ═══
items.forEach((_, i) => {
const lineAngle = rotation + i * slice;
const lGrad = ctx.createLinearGradient(
@@ -113,7 +140,7 @@ function draw(rotation) {
cx + r * Math.cos(lineAngle),
cy + r * Math.sin(lineAngle)
);
lGrad.addColorStop(0, 'rgba(700,550,0,5)');
lGrad.addColorStop(0, 'rgba(255,215,0,0.3)');
lGrad.addColorStop(0.4, '#a2922e');
lGrad.addColorStop(1, '#c8b43b');
ctx.beginPath();
@@ -124,31 +151,29 @@ function draw(rotation) {
ctx.stroke();
});
// ═══ متن آیتم‌ها ═══
items.forEach((item, i) => {
const start = rotation + i * slice;
const theme = getTheme(i, total);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(start + slice / 2);
ctx.textAlign = 'right';
ctx.textAlign = 'center';
ctx.fillStyle = theme.text;
ctx.font = 'bold 12px Tahoma';
ctx.font = 'bold 18px Dana';
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 4;
const text = item.title;
const maxWidth = r - 52;
if (ctx.measureText(text).width > maxWidth) {
const mid = Math.ceil(text.length / 2);
ctx.fillText(text.slice(0, mid), r - 20, -7);
ctx.fillText(text.slice(mid), r - 20, 9);
ctx.fillText(text.slice(0, mid), (r - 20) / 1.4, -9);
ctx.fillText(text.slice(mid), (r - 20) / 1.4, 9);
} else {
ctx.fillText(text, r - 20, 5);
ctx.fillText(text, (r - 20) / 1.4, 5);
}
ctx.restore();
});
// ═══ حلقه طلایی داخلی ═══
ctx.beginPath();
ctx.arc(cx, cy, r + 2, 0, Math.PI * 2);
ctx.strokeStyle = '#dfd498';
@@ -158,7 +183,6 @@ function draw(rotation) {
ctx.stroke();
ctx.shadowBlur = 0;
// ═══ لامپ‌ها ═══
const lampCount = 24;
const lampR = r + 23;
for (let i = 0; i < lampCount; i++) {
@@ -208,7 +232,6 @@ function draw(rotation) {
ctx.fill();
}
// ═══ حلقه طلایی بیرونی ═══
ctx.beginPath();
ctx.arc(cx, cy, r + 35, 0, Math.PI * 2);
ctx.strokeStyle = '#C8960C';
@@ -218,7 +241,6 @@ function draw(rotation) {
ctx.stroke();
ctx.shadowBlur = 0;
// ═══ دایره مرکزی — هماهنگ با نشانگر ═══
const cGrad = ctx.createRadialGradient(cx - 8, cy - 8, 1, cx, cy, 22);
cGrad.addColorStop(0, '#FFFFF0');
cGrad.addColorStop(0.15, '#FFE566');
@@ -240,18 +262,15 @@ function draw(rotation) {
ctx.lineWidth = 2;
ctx.stroke();
// بازتاب روی دایره مرکزی
ctx.beginPath();
ctx.arc(cx - 6, cy - 6, 7, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fill();
// ═══ نشانگر ═══
drawPointer(cx);
}
function drawPointer(cx) {
// سایه
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.4)';
ctx.shadowBlur = 8;
@@ -263,7 +282,6 @@ function drawPointer(cx) {
ctx.fill();
ctx.restore();
// دایره طلایی — همرنگ مرکز
const pGrad = ctx.createRadialGradient(cx - 6, 14, 1, cx, 20, 18);
pGrad.addColorStop(0, '#FFFFF0');
pGrad.addColorStop(0.15, '#FFE566');
@@ -282,13 +300,11 @@ function drawPointer(cx) {
ctx.lineWidth = 2;
ctx.stroke();
// بازتاب
ctx.beginPath();
ctx.arc(cx - 5, 13, 6, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.fill();
// مثلث — همرنگ
const triGrad = ctx.createLinearGradient(cx, 34, cx, 62);
triGrad.addColorStop(0, '#FFE566');
triGrad.addColorStop(0.35, '#FFD700');
@@ -309,6 +325,52 @@ function drawPointer(cx) {
ctx.stroke();
}
function playTickSound() {
try {
const actx = getAudioCtx();
const osc = actx.createOscillator();
const gain = actx.createGain();
osc.connect(gain);
gain.connect(actx.destination);
osc.frequency.value = 600;
osc.type = 'sine';
gain.gain.setValueAtTime(0.15, actx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + 0.08);
osc.start(actx.currentTime);
osc.stop(actx.currentTime + 0.08);
} catch(e) {}
}
function playWinSound() {
try {
const actx = getAudioCtx();
const notes = [523, 659, 784, 1047];
notes.forEach((freq, i) => {
const osc = actx.createOscillator();
const gain = actx.createGain();
osc.connect(gain);
gain.connect(actx.destination);
osc.frequency.value = freq;
osc.type = 'sine';
gain.gain.setValueAtTime(0.2, actx.currentTime + i * 0.15);
gain.gain.exponentialRampToValueAtTime(0.001, actx.currentTime + i * 0.15 + 0.3);
osc.start(actx.currentTime + i * 0.15);
osc.stop(actx.currentTime + i * 0.15 + 0.3);
});
} catch(e) {}
}
function showWinnerModal(winner) {
const modal = document.getElementById('winner-modal');
const iconMap = { 'پوچ': '', 'کد تخفیف': '', 'لایوکوین': '' };
document.getElementById('modal-icon').textContent = iconMap[winner.prize_type] || '🎉';
document.getElementById('modal-prize-type').textContent = winner.prize_type;
document.getElementById('modal-prize').textContent = winner.prize;
document.getElementById('modal-description').textContent = winner.description || '';
modal.classList.remove('hidden');
document.getElementById('modal-close').onclick = () => modal.classList.add('hidden');
}
btn.onclick = async () => {
if (spinning || items.length === 0) return;
spinning = true;
@@ -328,12 +390,20 @@ btn.onclick = async () => {
const duration = 5500;
const startTime = performance.now();
const from = angle;
let lastTickIdx = 0;
function animate(now) {
const elapsed = now - startTime;
const t = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - t, 4);
angle = from + (targetAngle - from) * ease;
const currentIdx = Math.floor(Math.abs(angle) / slice);
if (currentIdx !== lastTickIdx) {
lastTickIdx = currentIdx;
playTickSound();
}
draw(angle);
if (t < 1) {
@@ -342,7 +412,8 @@ btn.onclick = async () => {
spinning = false;
btn.disabled = false;
draw(angle);
result.innerHTML = `<span class="winner-text">🎉 ${winner.title} 🎉</span>`;
playWinSound();
showWinnerModal(winner);
}
}

View File

@@ -2,18 +2,34 @@
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>گردونه شانس</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<body id="main-body">
<div class="bg-rays"></div>
<div class="page">
<h1>گردونه شانس</h1>
<h1> گردونه شانس</h1>
<div class="wheel-wrapper">
<canvas id="wheel" width="500" height="500"></canvas>
</div>
<button id="spin-btn">بچرخون!</button>
<button id="spin-btn">چرخوندن!</button>
<div id="result"></div>
</div>
<!-- مودال برنده -->
<div id="winner-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-icon" id="modal-icon">🎉</div>
<div id="modal-prize-type"></div>
<div id="modal-prize"></div>
<div id="modal-description"></div>
<button id="modal-close">باشه!</button>
</div>
</div>
<script type="module" src="assets/js/wheel.js"></script>
</body>
</html>
</html>