Реализация виджета Карта дня на JavaScript и PHP для эзотерических сайтов

Разбираем механику «Карты дня»: готовые скрипты JS и PHP для ежедневного вовлечения аудитории. Поднимаем поведенческие факторы простым виджетом!

Функционал «Карта дня» — это один из самых популярных интерактивных элементов на эзотерических, психологических и развлекательных сайтах. Он удерживает аудиторию, повышает поведенческие факторы (время на сайте, глубина просмотра) и стимулирует пользователей возвращаться ежедневно за новой порцией вдохновения или совета.

В этой статье мы подробно разберем, как работает такой скрипт, какие технологии лучше выбрать для реализации, приведем готовые примеры кода (JavaScript + PHP) и обсудим нюансы безопасности и UX.


Архитектура решения: Клиентская vs Серверная часть

Прежде чем писать код, нужно определиться с архитектурой. Выбор зависит от требований к честности гадания и нагрузке на сервер.

Вариант А: Чистый JavaScript (Client-side only)

  • Плюсы: Нулевая нагрузка на сервер, работает на статическом хостинге (GitHub Pages, Netlify), мгновенный отклик.
  • Минусы: Пользователь может открыть консоль браузера, увидеть массив карт и логику выбора, подделать результат (изменить localStorage или дату системы).

Вариант Б: JavaScript + Backend (PHP / Node.js / Python)

  • Плюсы: Результат генерируется на сервере. Пользователь не может предугадать или подделать карту. Возможна привязка к авторизованному пользователю, сохранение истории, сбор статистики популярности карт.
  • Минусы: Требует сервера с поддержкой бэкенда, чуть сложнее в развертывании.
  • Для чего подходит: Серьезные эзотерические порталы, платные подписки, проекты, где важна «честность» рандома.

Подготовка данных: Колода карт

Основа любого гадания — это массив данных. Для Таро это 78 карт (22 Старших Аркана + 56 Младших). Для Оракулов — от 30 до 60 карт. Данные лучше хранить в JSON файле или таблице БД.

Пример структуры cards.json

[
 {
 "id": 0,
 "name": "Шут",
 "arcana": "major",
 "image": "assets/cards/0_fool.jpg",
 "meaning_upright": "Начало пути, спонтанность, вера в лучшее, риск.",
 "meaning_reversed": "Безрассудство, наивность, упущенные возможности, хаос.",
 "advice": "Доверьтесь интуиции, но не забывайте о безопасности. Новый этап требует смелости."
 }, {
 "id": 1,
 "name": "Маг",
 "arcana": "major",
 "image": "assets/cards/1_magician.jpg",
 "meaning_upright": "Воля, мастерство, манифестация, ресурсы.",
 "meaning_reversed": "Манипуляция, обман, нереализованный потенциал.",
 "advice": "У вас есть все инструменты для успеха. Действуйте уверенно."
 }
 // ... остальные 76 карт
]

Важно: Обязательно оптимизируйте изображения (WebP, размер ~50-100 Кб), чтобы виджет грузился мгновенно.


Реализация Варианта А: Чистый JavaScript (для статических сайтов)

Логика проста: берем текущую дату, превращаем её в число (seed), используем для детерминированного перемешивания массива (Fisher-Yates) и берем первую карту. Таким образом, в течение одного дня карта будет одинаковой при любом количестве обновлений страницы.

<div id="card-of-day" class="card-widget">
 <div class="card-loader">Загружаем вашу карту...</div>
 <div class="card-result" style="display: none;">
 <h3>Карта дня: <span id="card-name"></span></h3>
 <div class="card-image-wrapper">
 <img id="card-img" src="" alt="Карта дня" />
 <span id="card-orientation" class="orientation-badge"></span>
 </div>
 <div class="card-meaning">
 <h4>Значение (<span id="orient-text"></span>):</h4>
 <p id="card-desc"></p>
 </div>
 <div class="card-advice">
 <strong>Совет дня:</strong> <p id="card-advice-text"></p>
 </div>
 <button id="share-btn" class="btn">Поделиться</button>
 </div>
</div>

JavaScript логика (card-of-day.js)

// 1. Загрузка данных карт
let deckData = [];

async function loadDeck {
 try {
 const response = await fetch('/data/cards.json'); // Путь к вашему JSON
 if (!response.ok) throw new Error('Deck not found');
 deckData = await response.json;
 initCardOfDay;
 } catch (e) {
 console.error('Failed to load deck:', e);
 document.querySelector('.card-loader').textContent = 'Ошибка загрузки колоды.';
 }
}

// 2. Детерминированный рандом на основе даты (Seed Random)
function seededRandom(seed) {
 // Простой алгоритм LCG (Linear Congruential Generator)
 const x = Math.sin(seed) * 10000;
 return x ౼ Math.floor(x);
}

function shuffleArray(array, seed) {
 const arr = [...array]; // Копируем, чтобы не мутировать оригинал
 let counter = arr.length;
 let index = 0;
 let rand = seededRandom(seed); // Инициализируем сид
 while (counter > 0) {
 // Генерируем следующее псевдослучайное число
 rand = seededRandom(rand * 10000 + counter); 
 index = Math.floor(rand * counter);
 counter--;
 [arr[counter], arr[index]] = [arr[index], arr[counter]];
 }
 return arr;
}

// 3. Основная логика
function initCardOfDay {
 const today = new Date;
 // Создаем уникальный сид для текущего дня (YYYYMMDD)
 const dateSeed = today.getFullYear * 10000 + (today.getMonth + 1) * 100 + today.getDate;
 
 // Перемешиваем колоду с сидом дня
 const dailyDeck = shuffleArray(deckData, dateSeed);
 const card = dailyDeck[0]; // Первая карта = Карта дня

 // Определяем ориентацию (прямая/перевернутая) ౼ тоже детерминированно
 const orientationSeed = seededRandom(dateSeed * 1.5);
 const isReversed = orientationSeed > 0.5;

 renderCard(card, isReversed);
}

// 4. Рендер в DOM
function renderCard(card, isReversed) {
 document.querySelector('.card-loader').style.display = 'none';
 const resultEl = document.querySelector('.card-result');
 resultEl.style.display = 'block';

 document.getElementById('card-name').textContent = card.name;
 document.getElementById('card-img').src = card.image;
 document.getElementById('card-img').alt = card.name;

 const orientBadge = document.getElementById('card-orientation');
 const orientText = document.getElementById('orient-text');
 const descEl = document.getElementById('card-desc');
 const adviceEl = document.getElementById('card-advice-text');

 if (isReversed) {
 orientBadge.textContent = 'Перевернутая';
 orientBadge.classList.add('reversed');
 orientText.textContent = 'перевернутая';
 descEl.textContent = card.meaning_reversed;
 // Визуальный поворот карты
 document.getElementById('card-img').style.transform = 'rotate(180deg)';
 } else {
 orientBadge.textContent = 'Прямая';
 orientBadge.classList.remove('reversed');
 orientText.textContent = 'прямая';
 descEl.textContent = card.meaning_upright;
 document.getElementById('card-img').style.transform = 'rotate(0deg)';
 }
 
 adviceEl.textContent = card;advice;

 // Кнопка шаринга (Web Share API)
 document.getElementById('share-btn').onclick =  => shareCard(card, isReversed);
}

function shareCard(card, isReversed) {
 const text = `Моя карта дня: ${card.name} (${isReversed ? 'перевернутая' : 'прямая'}). ${card.advice}`;
 if (navigator.share) {
 navigator;share({ title: 'Карта дня', text: text, url: window.location.href });
 } else {
 navigator.clipboard.writeText(text + ' ' + window.location.href);
 alert('Ссылка скопирована в буфер обмена!');
 }
}
// Запуск
document.addEventListener('DOMContentLoaded', loadDeck);

Реализация Варианта Б: PHP Backend (Безопасность и История)

PHP Скрипт (api/card-of-day.php)

<?php
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *'); // Настройте под свой домен!
// 1. Подключение БД (PDO) или чтение JSON файла
$deckFile = __DIR__ . '/../data/cards.json';
if (!file_exists($deckFile)) {
 http_response_code(500);
 echo json_encode(['error' => 'Deck data missing']);
 exit;
}
$deck = json_decode(file_get_contents($deckFile), true);

// 2. Генерация сида на основе даты (и ID пользователя, если авторизован)
$dateSeed = (int)date('Ymd'); // 20260720
$userId = isset($_COOKIE['user_id']) ? (int)$_COOKIE['user_id'] : 0; // Пример привязки к юзеру
$finalSeed = $dateSeed + $userId * 10000; // Уникальный сид для юзера в этот день

// 3. Детерминированное перемешивание (Fisher-Yates на PHP)
mt_srand($finalSeed); // Инициализируем генератор сидом
$shuffledDeck = $deck;
$count = count($shuffledDeck);
for ($i = $count ౼ 1; $i > 0; $i--) {
 $j = mt_rand(0, $i);
 [$shuffledDeck[$i], $shuffledDeck[$j]] = [$shuffledDeck[$j], $shuffledDeck[$i]];
}
$card = $shuffledDeck[0];

// 4. Ориентация
$orientationRand = mt_rand(0, 1); // 0 или 1
$isReversed = (bool)$orientationRand;

// 5. Сохранение в историю (если есть БД и юзер)
// $pdo->prepare("INSERT INTO history (user_id, card_id, orientation, date) VALUES (?, ?, ?, CURDATE)")->execute([$userId, $card['id'], $isReversed]);

// 6. Ответ
echo json_encode([
 'success' => true,
 'card' => $card,
 'orientation' => $isReversed ? 'reversed' : 'upright',
 'date' => date('Y-m-d')
]);
?>

Фронтенд для PHP версии (AJAX)

async function fetchCardOfDay {
 const loader = document.querySelector('.card-loader');
 const result = document.querySelector('.card-result');
 
 try {
 const resp = await fetch('/api/card-of-day.php');
 const data = await resp.json;
 
 if (!data.success) throw new Error(data.error);
 
 loader.style.display = 'none';
 result.style.display = 'block';
  // Рендер аналогично JS версии, но берем данные из data.card
 renderCard(data.card, data.orientation === 'reversed');
  } catch (err) {
 loader.textContent = 'Ошибка соединения с сервером гаданий.';
 console.error(err);
 }
}

document.addEventListener('DOMContentLoaded', fetchCardOfDay);

UX/UI: Делаем виджет живым и красивым

Просто показать картинку и текст — скучно. Современные пользователи ждут интерактива.

Идеи для улучшения:

  1. Анимация переворота (Flip Animation): Используйте CSS 3D transforms (transform-style: preserve-3d). При загрузке карта «лежит» рубашкой вверх, затем плавно переворачивается, раскрывая лицо.
  2. Анимация тасования: Перед появлением результата покажите быструю анимацию смены карт (как в слот-машине), используя requestAnimationFrame или CSS keyframes.
  3. Звуковое сопровождение: Легкий звук перелистывания карт или мистический гонг при открытии (с кнопкой «Выкл звук»!).
  4. Персонализация: Если пользователь авторизован, показывайте «Ваша карта дня», а не просто «Карта дня». Добавьте кнопку «Сохранить в дневник».
  5. Виджет «Карта вчера/завтра»: Не давайте гадать на завтра (это ломает ретеншн), но покажите карту вчерашнего дня с надписью «Как прошел вчерашний день?», это вызывает желание сравнить прогноз с реальностью.

Пример CSS для Flip-эффекта

.card-inner {
 position: relative;
 width: 100%;
 max-width: 300px;
 aspect-ratio: 2/3.5;
 transform-style: preserve-3d;
 transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
 cursor: pointer;
}
.card-inner.flipped {
 transform: rotateY(180deg);
}.card-face {
 position: absolute;
 width: 100%;
 height: 100%;
 backface-visibility: hidden;
 border-radius: 8px;
 box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.card-back {
 background: linear-gradient(145deg, #1a1a2e, #16213e);
 border: 2px solid #e94560;
 display: flex; align-items: center; justify-content: center;
 font-size: 1.2rem; color: #fff; text-align: center; padding: 20px;
 transform: rotateY(180deg); /* Рубашка изначально перевернута */
}
.card-front img { width: 100%; height: 100%; object-fit: cover; border-radius: 8px; }

Юридические и этические аспекты (Disclaimer)

Критически важно разместить в футере виджета или в модальном окне при первом посещении:

Дисклеймер: Гадание на картах Таро/Оракулах носит исключительно развлекательный и рефлексивный характер. Результаты генерации основаны на псевдослучайных алгоритмах и не являются предсказанием будущего, медицинской, юридической или финансовой консультацией. Принимая решения, опирайтесь на здравый смысл и профессиональную помощь специалистов.

Это защищает вас от претензий пользователей, которые могут принять совет карты («Рискни!» — и человек потерял деньги) как прямой указ к действию;


SEO и микроразметка (Schema.org)

Чтобы виджет приносил трафик из поиска, размечайте контент схемой Game или CreativeWork, либо кастомной схемой TarotReading (если используете расширения).

<div itemscope itemtype="https://schema.org/Game">
 <meta itemprop="name" content="Карта дня: Таро онлайн" />
 <meta itemprop="description" content="Ежедневное гадание на карте Таро. Узнайте совет дня бесплатно." />
 <div itemprop="image" itemscope itemtype="https://schema.org/ImageObject">
 <img itemprop="contentUrl" src="..." />
 <meta itemprop="caption" content="Карта дня: Шут" />
 </div>
 <div itemprop="text">...текст значения...</div>
</div>

Также настройте Open Graph и Twitter Cards для репостов: когда пользователь делится картой, в соцсетях должна подтягиваться именно его карта с картинкой и текстом совета.


Чек-лист перед запуском

  • [ ] Колода полная: Все 78 карт (или нужное кол-во) имеют уникальные ID, названия, изображения, значения Прямая/Перевернутая, Совет.
  • [ ] Изображения оптимизированы: WebP/AVIF, правильные размеры, настроен кэширование (Cache-Control: max-age=31536000 для статики).
  • [ ] Детерминизм работает: При обновлении страницы (F5) в течение суток карта не меняется. В полночь (по серверу или UTC) карта меняется.
  • [ ] Адаптивность: Виджет выглядит идеально на 320px, 768px, 1920px. Карта не уезжает за экран на мобильных.
  • [ ] Доступность (a11y): Есть alt у картинок, контрастность текста нормальная, управление с клавиатуры (Tab/Enter) работает.
  • [ ] Дисклеймер: Виден пользователю перед первым гаданием.
  • [ ] Аналитика: Навешаны события на: card_revealed (карта открыта), card_shared (поделились), orientation_reversed (выпала перевернутая) — для понимания вовлеченности.

Создание скрипта «Карта дня» — это отличная задача для пет-проекта или монетизируемого виджета. Главный секрет успеха — не в сложном рандоме (Math.random тут враг), а в детерминированности на основе даты (Seed), качественном контенте (тексты значений должны быть глубокими, а не шаблонными) и приятном UX (анимация переворота, шаринг).

Начните с простой JS-версии на статическом хостинге, протестируйте гипотезу на аудитории, а затем при необходимости перенесите логику на бэкенд для добавления истории, авторизации и защиты от манипуляций.

Удачи в разработке! Пусть вашим пользователям всегда выпадают Солнце, Звезда и Мир в прямом положении. ✨