diff --git a/apple-touch-icon.png b/apple-touch-icon.png new file mode 100644 index 0000000..d747f7f Binary files /dev/null and b/apple-touch-icon.png differ diff --git a/assets/app.js b/assets/app.js new file mode 100644 index 0000000..ac04a38 --- /dev/null +++ b/assets/app.js @@ -0,0 +1,93 @@ +const form = document.getElementById("form"); +const file = document.getElementById("file"); +const nameI = document.getElementById("username"); +const btn = document.getElementById("submit"); +const toasts = document.getElementById("toasts"); +const segSkin = document.getElementById("seg-skin"); +const segCape = document.getElementById("seg-cape"); + +let kind = "skin"; + +segSkin.onclick = () => { + kind = "skin"; + segSkin.classList.add("active"); + segCape.classList.remove("active"); + segSkin.setAttribute("aria-selected", "true"); + segCape.setAttribute("aria-selected", "false"); +}; + +segCape.onclick = () => { + kind = "cape"; + segCape.classList.add("active"); + segSkin.classList.remove("active"); + segCape.setAttribute("aria-selected", "true"); + segSkin.setAttribute("aria-selected", "false"); +}; + +function toast(msg, type = "ok") { + const t = document.createElement("div"); + t.className = "toast " + type; + t.innerHTML = + '
' + + msg + + '
'; + toasts.appendChild(t); + const close = () => { + t.style.transition = "opacity .15s ease, transform .15s ease"; + t.style.opacity = "0"; + t.style.transform = "translateY(-6px)"; + setTimeout(() => t.remove(), 180); + }; + t.querySelector(".x").onclick = close; + setTimeout(close, 4200); +} + +function validName(v) { + return /^[A-Za-z0-9_]{2,16}$/.test(v); +} + +form.addEventListener("submit", async (e) => { + e.preventDefault(); + const v = nameI.value.trim(); + if (!validName(v)) { + toast("Неправильний нік.", "err"); + return; + } + if (!file.files?.length) { + toast("Оберіть PNG.", "err"); + return; + } + + const fd = new FormData(); + fd.append("username", v); + fd.append("file", file.files[0]); + + const url = kind === "cape" ? "/upload-cape" : "/upload"; + + btn.disabled = true; + btn.textContent = "Завантаження…"; + try { + const res = await fetch(url, { method: "POST", body: fd }); + const text = await res.text(); + let data; + try { + data = JSON.parse(text); + } catch {} + if (!res.ok || !data || data.status !== "ok") { + toast(data && data.error ? data.error : "Помилка завантаження.", "err"); + } else { + toast( + kind === "cape" ? "Готово. Плащ збережено." : "Готово. Скін збережено.", + "ok" + ); + form.reset(); + kind = "skin"; + segSkin.click(); + } + } catch { + toast("Мережа недоступна.", "err"); + } finally { + btn.disabled = false; + btn.textContent = "Завантажити"; + } +}); diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..6c72065 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,222 @@ +:root { + --mantle: #181825; + --crust: #11111b; + --text: #cdd6f4; + --muted: #a6adc8; + --line: #45475a; + --lav: #b4befe; + --sky: #89dceb; + --green: #a6e3a1; + --red: #f38ba8; + --shadow: 0 12px 40px rgba(0, 0, 0, 0.35); +} + +* { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +body { + margin: 0; + background: linear-gradient(160deg, var(--mantle), var(--crust)); + color: var(--text); + font: 16px/1.5 -apple-system, BlinkMacSystemFont, Segoe UI, Inter, Roboto, + Ubuntu, Arial, sans-serif; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} + +.shell { + width: 100%; + max-width: 520px; +} + +.card { + background: linear-gradient( + 180deg, + rgba(49, 50, 68, 0.9), + rgba(24, 24, 37, 0.92) + ); + border: 1px solid var(--line); + border-radius: 24px; + padding: 26px; + box-shadow: var(--shadow); + backdrop-filter: blur(8px); +} + +.h { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 18px; +} + +.h1 { + font-size: clamp(22px, 4vw, 28px); + font-weight: 700; + letter-spacing: 0.2px; +} + +.form { + display: grid; + gap: 16px; +} + +.lbl { + font-size: 13px; + color: var(--muted); +} + +.input { + width: 100%; + background: #2b2d3a; + border: 1px solid var(--line); + color: var(--text); + border-radius: 14px; + padding: 14px 16px; + outline: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.input:focus { + border-color: var(--lav); + box-shadow: 0 0 0 4px rgba(180, 190, 254, 0.18); +} + +/* Segmented control */ +.segment { + background: #2b2d3a; + border: 1px solid var(--line); + border-radius: 14px; + padding: 4px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; +} + +.segbtn { + border: 0; + border-radius: 10px; + padding: 10px 12px; + cursor: pointer; + color: var(--text); + background: transparent; + font-weight: 700; +} + +.segbtn.active { + background: linear-gradient(90deg, var(--lav), var(--sky)); + color: #0b0b10; +} + +/* Button */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + margin-top: 6px; + padding: 14px 16px; + border-radius: 14px; + border: 0; + cursor: pointer; + background: linear-gradient(90deg, var(--lav), var(--sky)); + color: #0b0b10; + font-weight: 800; + transition: transform 0.06s ease, filter 0.2s ease; +} + +.btn:active { + transform: translateY(1px); +} + +.btn[disabled] { + filter: saturate(0.2) brightness(0.85); + cursor: default; +} + +/* Toasts (top-right) */ +#toasts { + position: fixed; + right: 16px; + top: 16px; + display: grid; + gap: 10px; + z-index: 9999; +} + +.toast { + background: #2b2d3a; + border: 1px solid var(--line); + color: var(--text); + padding: 12px 14px; + border-radius: 14px; + min-width: 260px; + box-shadow: var(--shadow); + display: flex; + gap: 10px; + align-items: flex-start; + animation: slideIn 0.25s ease both; +} + +.toast.ok { + border-color: var(--green); +} + +.toast.err { + border-color: var(--red); +} + +.toast .dot { + width: 10px; + height: 10px; + border-radius: 999px; + margin-top: 5px; +} + +.toast.ok .dot { + background: var(--green); +} + +.toast.err .dot { + background: var(--red); +} + +.toast .x { + margin-left: auto; + cursor: pointer; + opacity: 0.6; +} + +.credit { + position: fixed; + right: 18px; + bottom: 16px; + color: var(--muted); + font-size: 13px; + text-decoration: none; + transition: color 0.2s ease; + opacity: 0.75; +} +.credit:hover { + color: var(--lav); + opacity: 1; +} + +@keyframes slideIn { + from { + transform: translateY(-10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} diff --git a/favicon-16x16.png b/favicon-16x16.png new file mode 100644 index 0000000..8c07906 Binary files /dev/null and b/favicon-16x16.png differ diff --git a/favicon-32x32.png b/favicon-32x32.png new file mode 100644 index 0000000..612c959 Binary files /dev/null and b/favicon-32x32.png differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..4db570f Binary files /dev/null and b/favicon.ico differ diff --git a/index.php b/index.php new file mode 100644 index 0000000..ca327ed --- /dev/null +++ b/index.php @@ -0,0 +1,141 @@ + base() . "/skins/{$name}.png", 'username' => $name]); +} + +/* === API: upload cape === */ +if ($method === 'POST' && $path === '/upload-cape') { + $name = $_POST['username'] ?? ''; + if (!preg_match('/^[A-Za-z0-9_]{2,16}$/', $name)) return err('Неправильний нік (2–16, латиниця/цифри/_).'); + $tmp = require_png_upload('file'); + + [$w, $h] = must_image_size($tmp); + // Плащ: співвідношення 2:1, мінімум 64×32, кратні двійці (типові 64×32, 128×64, 256×128, …) + if (!($w === 2 * $h && $w >= 64 && $h >= 32)) { + return err("Плащ має бути зі співвідношенням 2:1 (64×32, 128×64, …). Отримано {$w}×{$h}."); + } + $img = @imagecreatefrompng($tmp) ?: err('Пошкоджений PNG.'); + + if (!is_dir(__DIR__ . '/capes')) @mkdir(__DIR__ . '/capes', 0755, true); + $dest = __DIR__ . "/capes/{$name}.png"; + @imagepng($img, $dest) ?: err('Не вдалося зберегти (права на ./capes).'); + imagedestroy($img); + + json_ok(['cape_url' => base() . "/capes/{$name}.png", 'username' => $name]); +} + +/* === UI === */ +if ($method === 'GET' && $path === '/') { + header('Content-Type: text/html; charset=utf-8'); + echo << + + + + + +MSS - MrAkells Skin Server + + + + + + + + + + +
+
+
+
+ + +
+ +
+
Нікнейм
+ +
+
+
Файл PNG
+ +
+ +
+
+
+ +
+ + by MrAkells + + + + +HTML; + exit; +} + +/* === 404 === */ +http_response_code(404); +header('Content-Type: text/plain; charset=utf-8'); +echo "Not Found"; +exit; + +/* === helpers === */ +function err(string $m): void +{ + header('Content-Type: application/json; charset=utf-8', true, 400); + echo json_encode(['status' => 'error', 'error' => $m], JSON_UNESCAPED_UNICODE); + exit; +} +function json_ok(array $extra): void +{ + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['status' => 'ok'] + $extra, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} +function require_png_upload(string $key): string +{ + if (!isset($_FILES[$key]) || $_FILES[$key]['error'] !== UPLOAD_ERR_OK) err('Файл не завантажено.'); + $tmp = $_FILES[$key]['tmp_name']; + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $tmp); + finfo_close($finfo); + if ($mime !== 'image/png') err('Лише PNG.'); + return $tmp; +} +function must_image_size(string $tmp): array +{ + $info = @getimagesize($tmp); + if (!$info) err('Не PNG.'); + return [$info[0], $info[1]]; +} +function base(): string +{ + $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['SERVER_PORT'] ?? '') === '443'); + $scheme = $https ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; + return $scheme . '://' . $host; +}