SHA256
Добавлен SHiNE-promo-solana-devnet в основной репозиторий без вложенного git
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user