Добавлен SHiNE-promo-solana-devnet в основной репозиторий без вложенного git

This commit is contained in:
AidarKC
2026-05-01 13:58:59 +03:00
parent 9b03273055
commit 3061bf3d1e
25 changed files with 1812 additions and 0 deletions
@@ -0,0 +1,17 @@
server.port=8021
spring.application.name=SHiNE-promo-solana-devnet
spring.thymeleaf.cache=false
solana.rpc.url=https://api.devnet.solana.com
solana.sender.keypair-file=./config/devnet-wallet.json
promo.transfer.amount-sol=0.1
promo.codes.file=./data/promo-codes.txt
promo.used.file=./data/promo-used.txt
promo.explorer.tx-url-template=https://explorer.solana.com/tx/%s?cluster=devnet
# Вечный промокод для временной раздачи в devnet.
# Если enabled=true, код можно использовать неограниченно (он не "сгорает").
promo.eternal-code.enabled=true
promo.eternal-code.value=0000
@@ -0,0 +1,200 @@
:root {
--bg-main: #070707;
--bg-card: #111217;
--bg-card-soft: #171a22;
--text-main: #f6f8fb;
--text-muted: #aeb4c1;
--accent: #51ffd3;
--accent-soft: rgba(81, 255, 211, 0.15);
--danger: #ff6f6f;
--border: rgba(255, 255, 255, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Segoe UI", "Noto Sans", system-ui, sans-serif;
color: var(--text-main);
background:
radial-gradient(circle at 20% 5%, rgba(81, 255, 211, 0.08), transparent 34%),
radial-gradient(circle at 100% 0%, rgba(86, 135, 255, 0.12), transparent 40%),
var(--bg-main);
min-height: 100vh;
}
.page {
min-height: calc(100vh - 48px);
display: grid;
place-items: center;
padding: 24px 14px;
}
.card {
width: min(720px, 100%);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
border: 1px solid var(--border);
border-radius: 18px;
padding: 20px;
box-shadow: 0 12px 50px rgba(0, 0, 0, 0.45);
}
.hero h1 {
margin: 0;
font-size: clamp(1.3rem, 3.8vw, 1.9rem);
line-height: 1.2;
}
.subtitle {
color: var(--text-muted);
margin-top: 10px;
margin-bottom: 0;
}
.lead {
margin-top: 18px;
line-height: 1.5;
}
.warning {
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.03);
border-radius: 12px;
padding: 12px;
margin-top: 16px;
color: #f0f4ff;
}
.promo-form {
display: grid;
gap: 10px;
margin-top: 18px;
}
.promo-form label {
font-size: 0.95rem;
color: var(--text-muted);
}
.promo-form input {
width: 100%;
background: var(--bg-card-soft);
color: var(--text-main);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 12px;
font-size: 1rem;
}
.promo-form input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.promo-form button {
margin-top: 8px;
border: none;
border-radius: 12px;
background: linear-gradient(140deg, #2dd4bf, #4cc9f0);
color: #0b1116;
font-weight: 700;
font-size: 1rem;
padding: 14px;
cursor: pointer;
}
.promo-form button:disabled {
opacity: 0.7;
cursor: default;
}
.status {
margin-top: 14px;
padding: 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
}
.status-error {
color: #ffd6d6;
border-color: rgba(255, 111, 111, 0.6);
background: rgba(255, 111, 111, 0.12);
}
.success {
margin-top: 16px;
border: 1px solid rgba(81, 255, 211, 0.35);
background: rgba(81, 255, 211, 0.08);
border-radius: 12px;
padding: 14px;
}
.success h2 {
margin-top: 0;
font-size: 1.12rem;
}
.success p {
margin-top: 8px;
line-height: 1.45;
}
.success ul {
padding-left: 18px;
margin: 10px 0;
display: grid;
gap: 6px;
word-break: break-word;
}
.success a {
color: var(--accent);
}
.faq {
margin-top: 20px;
}
.faq h3 {
margin-bottom: 10px;
}
.faq details {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 12px;
margin-bottom: 8px;
}
.faq summary {
cursor: pointer;
font-weight: 600;
}
.faq p {
margin-bottom: 4px;
color: var(--text-muted);
line-height: 1.45;
}
.page-footer {
text-align: center;
color: #727a88;
font-size: 0.8rem;
padding-bottom: 18px;
}
.hidden {
display: none;
}
@media (min-width: 768px) {
.card {
padding: 26px;
}
}
@@ -0,0 +1,82 @@
(() => {
const form = document.getElementById("promoForm");
const walletInput = document.getElementById("wallet");
const nameInput = document.getElementById("name");
const promoCodeInput = document.getElementById("promoCode");
const submitButton = document.getElementById("submitButton");
const loadingState = document.getElementById("loadingState");
const errorState = document.getElementById("errorState");
const successState = document.getElementById("successState");
const successAmount = document.getElementById("successAmount");
const successWallet = document.getElementById("successWallet");
const successName = document.getElementById("successName");
const successSignature = document.getElementById("successSignature");
const successExplorerUrl = document.getElementById("successExplorerUrl");
const params = new URLSearchParams(window.location.search);
const walletFromQuery = (params.get("wallet") || "").trim();
if (walletFromQuery && !walletInput.value.trim()) {
walletInput.value = walletFromQuery;
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
hideMessages();
toggleLoading(true);
try {
const payload = {
wallet: walletInput.value.trim(),
name: nameInput.value.trim(),
promoCode: promoCodeInput.value.trim()
};
const response = await fetch("/api/promo/top-up", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const result = await response.json();
if (!result.success) {
showError(result.message || "Не удалось выполнить операцию");
return;
}
showSuccess(result);
} catch (error) {
showError("Ошибка сети или недоступен backend");
} finally {
toggleLoading(false);
}
});
function toggleLoading(active) {
submitButton.disabled = active;
loadingState.classList.toggle("hidden", !active);
submitButton.textContent = active ? "Отправляем..." : "Пополнить тестовый счёт";
}
function hideMessages() {
errorState.classList.add("hidden");
successState.classList.add("hidden");
}
function showError(message) {
errorState.textContent = message;
errorState.classList.remove("hidden");
}
function showSuccess(data) {
successAmount.textContent = data.amountSol || "0.1";
successWallet.textContent = data.wallet || "—";
successName.textContent = data.name || "—";
successSignature.textContent = data.signature || "—";
successExplorerUrl.href = data.explorerUrl || "#";
successState.classList.remove("hidden");
}
})();
@@ -0,0 +1,137 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SHiNE / Сияние — тестовое пополнение</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<main class="page">
<section class="card">
<header class="hero">
<h1>SHiNE / Сияние — тестовое пополнение</h1>
<p class="subtitle">
Временная devnet-страница для приглашённых тестеров Web3-социальной сети SHiNE.
</p>
</header>
<p class="lead">
Если вы получили промокод, введите его ниже. Мы отправим на ваш Solana devnet-кошелёк 0.1 SOL для
тестирования функций SHiNE.
</p>
<div class="warning">
Это тестовая сеть Solana Devnet. Эти SOL не являются настоящими деньгами и используются только для
проверки работы приложения.
</div>
<form id="promoForm" class="promo-form" novalidate>
<label for="wallet">Кошелёк Solana</label>
<input id="wallet" name="wallet" type="text" th:value="${wallet}" placeholder="Введите публичный адрес"
required>
<label for="name">Ваше имя / имя тестера</label>
<input id="name" name="name" type="text" maxlength="120" placeholder="Например, Иван Петров" required>
<label for="promoCode">Промокод</label>
<input id="promoCode" name="promoCode" type="text" maxlength="8" placeholder="Например, x7k3m2qn"
required>
<button id="submitButton" type="submit">Пополнить тестовый счёт</button>
</form>
<div id="loadingState" class="status hidden">Отправляем транзакцию...</div>
<div id="errorState" class="status status-error hidden"></div>
<section id="successState" class="success hidden" aria-live="polite">
<h2>Готово! Тестовое пополнение выполнено</h2>
<p>
На указанный devnet-кошелёк отправлено 0.1 SOL. Возвращайтесь в приложение SHiNE / Сияние и
продолжайте тестирование.
</p>
<p>
Можете закрыть эту страницу и продолжить регистрацию на сайте.
</p>
<ul>
<li><strong>Сумма:</strong> <span id="successAmount"></span> SOL</li>
<li><strong>Кошелёк:</strong> <span id="successWallet"></span></li>
<li><strong>Имя:</strong> <span id="successName"></span></li>
<li><strong>Подпись транзакции:</strong> <span id="successSignature"></span></li>
</ul>
<a id="successExplorerUrl" href="#" target="_blank" rel="noopener noreferrer">
Открыть транзакцию в Solana Explorer
</a>
</section>
<section class="faq">
<h3>FAQ</h3>
<details>
<summary>Что такое SHiNE / Сияние?</summary>
<p>
SHiNE / «Сияние» — это тестируемая Web3-социальная сеть, где аккаунты, ключи и часть действий
связаны с блокчейном Solana. Пользователь управляет своим кошельком, а не просто логином и
паролем на обычном сервере.
</p>
</details>
<details>
<summary>Зачем нужен тестовый баланс?</summary>
<p>
В SHiNE регистрация и некоторые действия требуют небольших списаний. Это помогает тестировать
реальную экономику приложения: регистрацию, переводы, сообщения, звонки и другие функции. Сейчас
всё работает в Solana Devnet, поэтому средства тестовые.
</p>
</details>
<details>
<summary>Сколько стоит регистрация?</summary>
<p>
Текущая тестовая стоимость регистрации в SHiNE — 0.1 SOL. Промо-страница отправляет стартовое
тестовое пополнение 0.1 SOL. Условия тестирования могут меняться по мере развития проекта.
</p>
</details>
<details>
<summary>Можно ли пригласить друга?</summary>
<p>
Да. После получения тестового баланса и регистрации в SHiNE вы сможете переводить часть тестовых
SOL другим пользователям, например друзьям или родственникам, чтобы вместе проверить сообщения,
звонки и взаимодействие внутри социальной сети.
</p>
</details>
<details>
<summary>Это настоящие деньги?</summary>
<p>
Нет. Это Solana Devnet — тестовая сеть. Devnet SOL не имеют реальной рыночной ценности и
используются только для разработки и тестирования.
</p>
</details>
<details>
<summary>Почему в Web3 действия платные?</summary>
<p>
В Web3 часть действий связана с транзакциями, хранением данных, подписями и сетевой
инфраструктурой. Зато такая модель позволяет строить систему без навязчивой рекламы и с большей
прозрачностью: пользователь понимает, за что платит, а ключи остаются у него.
</p>
</details>
<details>
<summary>Кто хранит мои ключи?</summary>
<p>
Эта промо-страница не просит и не хранит приватные ключи пользователя. Для пополнения нужен только
публичный адрес кошелька Solana Devnet.
</p>
</details>
</section>
</section>
</main>
<footer class="page-footer">SHiNE Devnet Promo · temporary testing page</footer>
<script src="/js/app.js"></script>
</body>
</html>