<?php
/**
* Однофайловая промо-рулетка
* Версия: 1.0
* Для использования:
* 1. Сохраните как wheel.php
* 2. Откройте в браузере
* 3. Введите email и крутите!
*/
class PromoWheel {
private $config;
private $spins_file;
private $settings_file;
public function __construct() {
$this->spins_file = __DIR__ . '/wheel_spins.json';
$this->settings_file = __DIR__ . '/wheel_settings.json';
$this->loadSettings();
$this->initFiles();
}
private function loadSettings() {
$default_settings = [
'segments' => 12,
'win_chance' => 0.3, // 30% шанс выигрыша
'spins_per_day' => 3,
'discount_enabled' => true,
'discount_min' => 5,
'discount_max' => 50,
'discount_step' => [1, 5],
'sectors' => [
['label' => '10% OFF', 'type' => 'promo_code', 'payload' => 'SAVE10', 'weight' => 3],
['label' => '20% OFF', 'type' => 'promo_code', 'payload' => 'SAVE20', 'weight' => 2],
['label' => 'FREE SHIPPING', 'type' => 'bonus', 'payload' => 'FREESHIP', 'weight' => 2],
['label' => 'BUY 1 GET 1', 'type' => 'bonus', 'payload' => 'BOGO', 'weight' => 1],
['label' => 'NO WIN', 'type' => 'no_win', 'payload' => '', 'weight' => 6],
],
'email_templates' => [
'win' => "Congratulations! You won: {prize_label}\nYour code: {payload}",
'no_win' => "Thanks for playing! Try again tomorrow.",
]
];
if (file_exists($this->settings_file)) {
$saved = json_decode(file_get_contents($this->settings_file), true);
$this->config = array_merge($default_settings, $saved);
} else {
$this->config = $default_settings;
file_put_contents($this->settings_file, json_encode($default_settings, JSON_PRETTY_PRINT));
}
}
private function initFiles() {
if (!file_exists($this->spins_file)) {
file_put_contents($this->spins_file, json_encode([]));
}
}
private function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
private function getTodayKey($email) {
return date('Y-m-d') . '|' . strtolower($email);
}
private function getSpinsForEmail($email) {
$spins = json_decode(file_get_contents($this->spins_file), true);
$today_key = $this->getTodayKey($email);
return isset($spins[$today_key]) ? $spins[$today_key] : 0;
}
private function incrementSpins($email) {
$spins = json_decode(file_get_contents($this->spins_file), true);
$today_key = $this->getTodayKey($email);
$spins[$today_key] = ($spins[$today_key] ?? 0) + 1;
file_put_contents($this->spins_file, json_encode($spins));
}
private function canSpin($email) {
return $this->getSpinsForEmail($email) < $this->config['spins_per_day'];
}
private function weightedPick($items) {
$total_weight = array_sum(array_column($items, 'weight'));
$random = mt_rand() / mt_getrandmax() * $total_weight;
$current_weight = 0;
foreach ($items as $item) {
$current_weight += $item['weight'];
if ($random <= $current_weight) {
return $item;
}
}
return end($items);
}
private function calculateDiscount($email) {
$spins_today = $this->getSpinsForEmail($email);
$base_discount = $this->config['discount_min'];
$step = mt_rand($this->config['discount_step'][0], $this->config['discount_step'][1]);
$current_discount = $base_discount + ($spins_today * $step);
return min($current_discount, $this->config['discount_max']);
}
private function sendEmail($email, $type, $data = []) {
$template = $this->config['email_templates'][$type] ?? '';
if (empty($template)) return false;
$message = str_replace(
['{prize_label}', '{payload}'],
[$data['prize_label'] ?? '', $data['payload'] ?? ''],
$template
);
// В реальном приложении здесь будет mail()
// mail($email, "Your Wheel Result", $message);
error_log("EMAIL to $email: $message"); // Для демонстрации
return true;
}
public function spin($email) {
if (!$this->validateEmail($email)) {
return ['success' => false, 'error' => 'Invalid email'];
}
if (!$this->canSpin($email)) {
return ['success' => false, 'error' => 'No spins left today'];
}
// Определяем результат
$win = mt_rand(1, 1000) <= ($this->config['win_chance'] * 1000);
$result = [];
if ($win) {
// Выбираем приз
$eligible_sectors = array_filter($this->config['sectors'], function($s) {
return $s['type'] !== 'no_win';
});
if (!empty($eligible_sectors)) {
$prize = $this->weightedPick(array_values($eligible_sectors));
$result = [
'type' => 'win',
'prize_label' => $prize['label'],
'payload' => $prize['payload'],
'sector_type' => $prize['type']
];
// Отправляем email
$this->sendEmail($email, 'win', [
'prize_label' => $prize['label'],
'payload' => $prize['payload']
]);
} else {
$win = false; // Нет призов
}
}
if (!$win) {
$result = [
'type' => 'no_win',
'prize_label' => 'NO WIN',
'payload' => '',
'sector_type' => 'no_win'
];
$this->sendEmail($email, 'no_win');
}
// Обновляем счетчик спинов
$this->incrementSpins($email);
// Рассчитываем discount
$discount = $this->config['discount_enabled'] ? $this->calculateDiscount($email) : 0;
// Вычисляем угол остановки (для визуализации)
$angle = mt_rand(0, 359);
return [
'success' => true,
'result' => $result,
'spins_used' => $this->getSpinsForEmail($email),
'spins_left' => max(0, $this->config['spins_per_day'] - $this->getSpinsForEmail($email)),
'discount_percent' => round($discount, 1),
'target_angle' => $angle,
'message' => $win ? "Congratulations!" : "Better luck next time!"
];
}
public function getConfig() {
return [
'segments' => $this->config['segments'],
'spins_per_day' => $this->config['spins_per_day'],
'win_chance' => $this->config['win_chance'],
'discount_enabled' => $this->config['discount_enabled'],
'discount_range' => [$this->config['discount_min'], $this->config['discount_max']]
];
}
public function getStats($email) {
if (!$this->validateEmail($email)) return null;
return [
'spins_used_today' => $this->getSpinsForEmail($email),
'spins_left_today' => max(0, $this->config['spins_per_day'] - $this->getSpinsForEmail($email)),
'discount_percent' => $this->config['discount_enabled'] ? $this->calculateDiscount($email) : 0
];
}
}
// API обработчик
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$wheel = new PromoWheel();
$input = json_decode(file_get_contents('php://input'), true);
$action = $_GET['action'] ?? '';
$email = $input['email'] ?? '';
switch ($action) {
case 'spin':
echo json_encode($wheel->spin($email));
break;
case 'config':
echo json_encode($wheel->getConfig());
break;
case 'stats':
echo json_encode($wheel->getStats($email));
break;
default:
echo json_encode(['success' => false, 'error' => 'Unknown action']);
}
exit;
}
// HTML интерфейс
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Promo Wheel</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', system-ui, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 30px;
width: 100%;
max-width: 500px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
}
h1 {
text-align: center;
color: white;
margin-bottom: 30px;
font-size: 2.5rem;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 8px;
font-weight: 500;
}
input[type="email"] {
width: 100%;
padding: 15px;
border-radius: 12px;
border: 2px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.15);
color: white;
font-size: 1rem;
outline: none;
transition: all 0.3s ease;
}
input[type="email"]:focus {
border-color: #ff6b6b;
background: rgba(255, 255, 255, 0.25);
}
input[type="email"]::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.btn {
width: 100%;
padding: 16px;
border-radius: 12px;
border: none;
background: linear-gradient(45deg, #ff6b6b, #ffa502);
color: white;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(255, 107, 107, 0.4);
}
.btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(255, 107, 107, 0.6);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.stats {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
margin-top: 25px;
}
.stat-item {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
color: white;
}
.stat-value {
font-weight: 600;
color: #ff6b6b;
}
.result {
background: rgba(255, 255, 255, 0.15);
border-radius: 15px;
padding: 25px;
margin-top: 25px;
text-align: center;
display: none;
}
.result.win {
border: 2px solid #4caf50;
background: rgba(76, 175, 80, 0.15);
}
.result.lose {
border: 2px solid #f44336;
background: rgba(244, 67, 54, 0.15);
}
.result-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 15px;
}
.prize-label {
font-size: 1.8rem;
font-weight: 800;
margin: 10px 0;
color: #ffeb3b;
text-shadow: 0 0 10px rgba(255, 235, 59, 0.5);
}
.discount-badge {
background: linear-gradient(45deg, #2196f3, #ff9800);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
margin-top: 15px;
display: inline-block;
}
.wheel-container {
position: relative;
width: 300px;
height: 300px;
margin: 30px auto;
display: none;
}
.wheel {
width: 100%;
height: 100%;
border-radius: 50%;
position: relative;
transition: transform 4s cubic-bezier(0.17, 0.67, 0.12, 0.99);
background: conic-gradient(
#ff6b6b 0deg 30deg,
#4ecdc4 30deg 60deg,
#45b7d1 60deg 90deg,
#96ceb4 90deg 120deg,
#feca57 120deg 150deg,
#ff9ff3 150deg 180deg,
#a29bfe 180deg 210deg,
#fd79a8 210deg 240deg,
#fdcb6e 240deg 270deg,
#6c5ce7 270deg 300deg,
#a29bfe 300deg 330deg,
#fd79a8 330deg 360deg
);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.pointer {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 30px;
background: #ffeb3b;
clip-path: polygon(50% 100%, 0 0, 100% 0);
z-index: 10;
box-shadow: 0 0 15px rgba(255, 235, 59, 0.8);
}
.error {
background: rgba(244, 67, 54, 0.2);
border: 1px solid #f44336;
color: #ff6b6b;
padding: 15px;
border-radius: 10px;
margin-top: 15px;
display: none;
}
@media (max-width: 600px) {
.container {
padding: 20px;
}
h1 {
font-size: 2rem;
}
.wheel-container {
width: 250px;
height: 250px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🎰 Promo Wheel</h1>
<form id="wheelForm">
<div class="form-group">
<label for="email">Your Email</label>
<input type="email" id="email" placeholder="Enter your email to play" required>
</div>
<button type="submit" class="btn" id="spinBtn">SPIN THE WHEEL</button>
</form>
<div class="error" id="errorMessage"></div>
<div class="wheel-container" id="wheelContainer">
<div class="pointer"></div>
<div class="wheel" id="wheel"></div>
</div>
<div class="result" id="result">
<div class="result-title" id="resultTitle">Result</div>
<div class="prize-label" id="prizeLabel">-</div>
<div id="resultMessage"></div>
<div class="discount-badge" id="discountBadge"></div>
</div>
<div class="stats" id="stats">
<div class="stat-item">
<span>Spins Today:</span>
<span class="stat-value" id="spinsUsed">0</span>
</div>
<div class="stat-item">
<span>Spins Left:</span>
<span class="stat-value" id="spinsLeft">0</span>
</div>
<div class="stat-item">
<span>Your Discount:</span>
<span class="stat-value" id="discountPercent">0%</span>
</div>
</div>
</div>
<script>
class WheelGame {
constructor() {
this.form = document.getElementById('wheelForm');
this.emailInput = document.getElementById('email');
this.spinBtn = document.getElementById('spinBtn');
this.errorMessage = document.getElementById('errorMessage');
this.wheelContainer = document.getElementById('wheelContainer');
this.wheel = document.getElementById('wheel');
this.result = document.getElementById('result');
this.resultTitle = document.getElementById('resultTitle');
this.prizeLabel = document.getElementById('prizeLabel');
this.resultMessage = document.getElementById('resultMessage');
this.discountBadge = document.getElementById('discountBadge');
this.spinsUsed = document.getElementById('spinsUsed');
this.spinsLeft = document.getElementById('spinsLeft');
this.discountPercent = document.getElementById('discountPercent');
this.init();
}
init() {
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.spinWheel();
});
// Загружаем начальную статистику
this.loadStats();
}
showError(message) {
this.errorMessage.textContent = message;
this.errorMessage.style.display = 'block';
setTimeout(() => {
this.errorMessage.style.display = 'none';
}, 5000);
}
async apiCall(action, data = {}) {
const response = await fetch(`?action=${action}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
return await response.json();
}
async loadStats() {
const email = this.emailInput.value.trim();
if (!email) return;
try {
const stats = await this.apiCall('stats', { email });
if (stats) {
this.spinsUsed.textContent = stats.spins_used_today;
this.spinsLeft.textContent = stats.spins_left_today;
this.discountPercent.textContent = `${stats.discount_percent}%`;
}
} catch (error) {
console.error('Error loading stats:', error);
}
}
async spinWheel() {
const email = this.emailInput.value.trim();
if (!email) {
this.showError('Please enter your email');
return;
}
this.spinBtn.disabled = true;
this.spinBtn.textContent = 'SPINNING...';
try {
// Показываем колесо
this.wheelContainer.style.display = 'block';
// Анимация вращения
const spins = 5 + Math.random() * 5; // 5-10 оборотов
const targetRotation = spins * 360 + (Math.random() * 360);
this.wheel.style.transform = `rotate(${targetRotation}deg)`;
// Ждем окончания анимации
await new Promise(resolve => setTimeout(resolve, 4000));
// Получаем результат
const response = await this.apiCall('spin', { email });
if (response.success) {
this.showResult(response);
await this.loadStats();
} else {
this.showError(response.error || 'Something went wrong');
}
} catch (error) {
this.showError('Network error. Please try again.');
console.error('Error:', error);
} finally {
this.spinBtn.disabled = false;
this.spinBtn.textContent = 'SPIN THE WHEEL';
}
}
showResult(data) {
this.result.className = `result ${data.result.type}`;
this.result.style.display = 'block';
if (data.result.type === 'win') {
this.resultTitle.textContent = '🎉 Congratulations!';
this.prizeLabel.textContent = data.result.prize_label;
this.resultMessage.textContent = 'You won a special prize!';
} else {
this.resultTitle.textContent = '😅 No Luck This Time';
this.prizeLabel.textContent = 'Try Again Tomorrow';
this.resultMessage.textContent = 'Keep playing to increase your discount!';
}
this.discountBadge.textContent = `Your Discount: ${data.discount_percent}%`;
// Автоматически скрываем результат через 5 секунд
setTimeout(() => {
this.result.style.display = 'none';
}, 5000);
}
}
// Инициализируем игру когда страница загрузится
document.addEventListener('DOMContentLoaded', () => {
new WheelGame();
});
</script>
</body>
</html>