feat: redesign website templates with full pages
- Redesigned base.html: dark Minecraft theme, sticky header, responsive grid, cards, server cards with status indicators, profile styles - index.html: hero section, server list grid, how-to-start steps, features section - login.html: centered card layout, client-side validation, fetch API - registration.html: password confirmation, pattern validation, error alerts - profile.html: new page — skin/cape upload & delete, launcher download links, auth-gated via localStorage token - templates.go: added /profile route, extended pageData with Username/UUID
This commit is contained in:
240
internal/templates/html/profile.html
Normal file
240
internal/templates/html/profile.html
Normal file
@@ -0,0 +1,240 @@
|
||||
{{define "content"}}
|
||||
<div id="authCheck" class="card text-center">
|
||||
<p class="muted">Загрузка...</p>
|
||||
</div>
|
||||
|
||||
<div id="profileContent" style="display:none">
|
||||
<!-- Profile header -->
|
||||
<div class="card">
|
||||
<div class="profile-header">
|
||||
<img class="profile-avatar" id="skinPreview" src="" alt="Аватар" onerror="this.src='data:image/svg+xml,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 8 8%27><rect fill=%27%23334155%27 width=%278%27 height=%278%27/></svg>'">
|
||||
<div class="profile-info">
|
||||
<h2 id="profUsername">—</h2>
|
||||
<p class="uuid" id="profUuid">—</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skin & Cape -->
|
||||
<h2 class="section-title">👤 Скин и плащ</h2>
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<h3>Скин</h3>
|
||||
<div id="currentSkin" class="text-center" style="margin-bottom:1rem">
|
||||
<img id="skinImg" src="" alt="Скин" style="width:120px;height:160px;image-rendering:pixelated;background:var(--surface-light);border-radius:var(--radius-sm)" onerror="this.style.display='none'">
|
||||
<p class="muted text-sm mt-1" id="noSkinMsg">Скин не загружен</p>
|
||||
</div>
|
||||
<div id="alertSkin" class="alert"></div>
|
||||
<form id="skinForm" enctype="multipart/form-data">
|
||||
<label for="skinFile">Загрузить скин (PNG, 64×64 или 64×32)</label>
|
||||
<input type="file" id="skinFile" name="file" accept=".png">
|
||||
<div class="flex gap-1">
|
||||
<button type="submit" class="btn btn-sm">Загрузить</button>
|
||||
<button type="button" id="deleteSkin" class="btn btn-sm btn-danger">Удалить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Плащ</h3>
|
||||
<div id="currentCape" class="text-center" style="margin-bottom:1rem">
|
||||
<img id="capeImg" src="" alt="Плащ" style="width:100px;height:160px;image-rendering:pixelated;background:var(--surface-light);border-radius:var(--radius-sm)" onerror="this.style.display='none'">
|
||||
<p class="muted text-sm mt-1" id="noCapeMsg">Плащ не загружен</p>
|
||||
</div>
|
||||
<div id="alertCape" class="alert"></div>
|
||||
<form id="capeForm" enctype="multipart/form-data">
|
||||
<label for="capeFile">Загрузить плащ (PNG, 64×32)</label>
|
||||
<input type="file" id="capeFile" name="file" accept=".png">
|
||||
<div class="flex gap-1">
|
||||
<button type="submit" class="btn btn-sm">Загрузить</button>
|
||||
<button type="button" id="deleteCape" class="btn btn-sm btn-danger">Удалить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Launcher download -->
|
||||
<h2 class="section-title">💾 Лаунчер</h2>
|
||||
<div class="card">
|
||||
<p class="muted">Скачай лаунчер для своей операционной системы. Он автоматически загрузит все файлы модпаков.</p>
|
||||
<div class="flex gap-1 mt-2" id="launcherLinks">
|
||||
<a href="#" class="btn btn-sm" id="dl-win">🪟 Windows</a>
|
||||
<a href="#" class="btn btn-sm" id="dl-mac">🍎 macOS</a>
|
||||
<a href="#" class="btn btn-sm" id="dl-lin">🐧 Linux</a>
|
||||
</div>
|
||||
<p class="text-sm muted mt-2" id="latestVersion">Последняя версия: —</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const token = localStorage.getItem('token');
|
||||
const savedUuid = localStorage.getItem('uuid');
|
||||
const savedUsername = localStorage.getItem('username');
|
||||
|
||||
if (!token) {
|
||||
document.getElementById('authCheck').innerHTML = '<h2>🔒 Требуется авторизация</h2><p class="muted">Войди в аккаунт, чтобы увидеть профиль.</p><p class="mt-2"><a href="/login" class="btn">Войти</a></p>';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('authCheck').style.display = 'none';
|
||||
document.getElementById('profileContent').style.display = 'block';
|
||||
|
||||
// Load profile
|
||||
loadProfile();
|
||||
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const res = await fetch('/api/web/profile/' + savedUuid, {
|
||||
headers: {'Authorization': 'Bearer ' + token}
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to load');
|
||||
const data = await res.json();
|
||||
|
||||
document.getElementById('profUsername').textContent = data.username || savedUsername || '—';
|
||||
document.getElementById('profUuid').textContent = data.uuid || savedUuid || '—';
|
||||
|
||||
// Skin
|
||||
if (data.textures && data.textures.skin) {
|
||||
document.getElementById('skinImg').src = data.textures.skin;
|
||||
document.getElementById('skinImg').style.display = 'inline';
|
||||
document.getElementById('noSkinMsg').style.display = 'none';
|
||||
} else {
|
||||
document.getElementById('skinImg').style.display = 'none';
|
||||
document.getElementById('noSkinMsg').style.display = 'block';
|
||||
}
|
||||
|
||||
// Cape
|
||||
if (data.textures && data.textures.cape) {
|
||||
document.getElementById('capeImg').src = data.textures.cape;
|
||||
document.getElementById('capeImg').style.display = 'inline';
|
||||
document.getElementById('noCapeMsg').style.display = 'none';
|
||||
} else {
|
||||
document.getElementById('capeImg').style.display = 'none';
|
||||
document.getElementById('noCapeMsg').style.display = 'block';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Profile load error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load latest launcher version
|
||||
loadLauncherInfo();
|
||||
|
||||
async function loadLauncherInfo() {
|
||||
try {
|
||||
const res = await fetch('/api/launcher/latest');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
document.getElementById('latestVersion').textContent = 'Последняя версия: ' + (data.version || '—');
|
||||
if (data.downloads) {
|
||||
if (data.downloads.windows) document.getElementById('dl-win').href = data.downloads.windows;
|
||||
if (data.downloads.darwin) document.getElementById('dl-mac').href = data.downloads.darwin;
|
||||
if (data.downloads.linux) document.getElementById('dl-lin').href = data.downloads.linux;
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
// Skin upload
|
||||
document.getElementById('skinForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const file = document.getElementById('skinFile').files[0];
|
||||
if (!file) return;
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const alert = document.getElementById('alertSkin');
|
||||
try {
|
||||
const res = await fetch('/api/web/profile/skin', {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': 'Bearer ' + token},
|
||||
body: fd
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert.className = 'alert alert-error show';
|
||||
alert.textContent = data.error || 'Ошибка загрузки';
|
||||
} else {
|
||||
alert.className = 'alert alert-success show';
|
||||
alert.textContent = 'Скин загружен!';
|
||||
loadProfile();
|
||||
}
|
||||
} catch (err) {
|
||||
alert.className = 'alert alert-error show';
|
||||
alert.textContent = 'Ошибка сети';
|
||||
}
|
||||
});
|
||||
|
||||
// Cape upload
|
||||
document.getElementById('capeForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const file = document.getElementById('capeFile').files[0];
|
||||
if (!file) return;
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const alert = document.getElementById('alertCape');
|
||||
try {
|
||||
const res = await fetch('/api/web/profile/cape', {
|
||||
method: 'POST',
|
||||
headers: {'Authorization': 'Bearer ' + token},
|
||||
body: fd
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
alert.className = 'alert alert-error show';
|
||||
alert.textContent = data.error || 'Ошибка загрузки';
|
||||
} else {
|
||||
alert.className = 'alert alert-success show';
|
||||
alert.textContent = 'Плащ загружен!';
|
||||
loadProfile();
|
||||
}
|
||||
} catch (err) {
|
||||
alert.className = 'alert alert-error show';
|
||||
alert.textContent = 'Ошибка сети';
|
||||
}
|
||||
});
|
||||
|
||||
// Delete skin
|
||||
document.getElementById('deleteSkin').addEventListener('click', async function() {
|
||||
const alert = document.getElementById('alertSkin');
|
||||
try {
|
||||
const res = await fetch('/api/web/profile/skin', {
|
||||
method: 'DELETE',
|
||||
headers: {'Authorization': 'Bearer ' + token}
|
||||
});
|
||||
if (!res.ok) {
|
||||
alert.className = 'alert alert-error show';
|
||||
alert.textContent = 'Ошибка удаления';
|
||||
} else {
|
||||
alert.className = 'alert alert-success show';
|
||||
alert.textContent = 'Скин удалён';
|
||||
loadProfile();
|
||||
}
|
||||
} catch (err) {
|
||||
alert.className = 'alert alert-error show';
|
||||
alert.textContent = 'Ошибка сети';
|
||||
}
|
||||
});
|
||||
|
||||
// Delete cape
|
||||
document.getElementById('deleteCape').addEventListener('click', async function() {
|
||||
const alert = document.getElementById('alertCape');
|
||||
try {
|
||||
const res = await fetch('/api/web/profile/cape', {
|
||||
method: 'DELETE',
|
||||
headers: {'Authorization': 'Bearer ' + token}
|
||||
});
|
||||
if (!res.ok) {
|
||||
alert.className = 'alert alert-error show';
|
||||
alert.textContent = 'Ошибка удаления';
|
||||
} else {
|
||||
alert.className = 'alert alert-success show';
|
||||
alert.textContent = 'Плащ удалён';
|
||||
loadProfile();
|
||||
}
|
||||
} catch (err) {
|
||||
alert.className = 'alert alert-error show';
|
||||
alert.textContent = 'Ошибка сети';
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user