mirror of
https://github.com/101island/lolisland.us.git
synced 2026-03-01 03:49:42 +08:00
style: improve frontend interaction details
This commit is contained in:
@@ -49,9 +49,13 @@ const { titleSvg } = Astro.props;
|
||||
|
||||
// Displays
|
||||
const displays = {
|
||||
loginError: document.getElementById("login-error"),
|
||||
qqError: document.getElementById("qq-error"),
|
||||
regError: document.getElementById("reg-error"),
|
||||
loginError: document.getElementById("login-password-error"),
|
||||
loginUserError: document.getElementById("login-username-error"),
|
||||
loginPassError: document.getElementById("login-password-error"),
|
||||
qqError: document.getElementById("qq-id-error"),
|
||||
regError: document.getElementById("reg-password-error"),
|
||||
regUserError: document.getElementById("reg-username-error"),
|
||||
regPassError: document.getElementById("reg-password-error"),
|
||||
verifyCode: document.getElementById("verification-code-display"),
|
||||
dashAvatar: document.getElementById(
|
||||
"dashboard-avatar-img",
|
||||
@@ -81,9 +85,13 @@ const { titleSvg } = Astro.props;
|
||||
}
|
||||
|
||||
function clearErrors() {
|
||||
if (displays.loginError) displays.loginError.classList.add("hidden");
|
||||
if (displays.qqError) displays.qqError.classList.add("hidden");
|
||||
if (displays.regError) displays.regError.classList.add("hidden");
|
||||
[
|
||||
displays.loginUserError,
|
||||
displays.loginPassError,
|
||||
displays.qqError,
|
||||
displays.regUserError,
|
||||
displays.regPassError,
|
||||
].forEach((el) => el?.classList.add("hidden"));
|
||||
}
|
||||
|
||||
function updateDashboard() {
|
||||
@@ -109,14 +117,38 @@ const { titleSvg } = Astro.props;
|
||||
switchView("dashboard");
|
||||
}
|
||||
|
||||
function setLoading(btnId: string, isLoading: boolean, originalText: string) {
|
||||
const btn = document.getElementById(btnId) as HTMLButtonElement | null;
|
||||
if (!btn) return;
|
||||
|
||||
if (isLoading) {
|
||||
btn.dataset.originalText = originalText;
|
||||
btn.innerHTML = '<span class="btn-spinner"></span>';
|
||||
btn.disabled = true;
|
||||
} else {
|
||||
btn.innerHTML = btn.dataset.originalText || originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Auth Logic ---
|
||||
async function performLogin() {
|
||||
const u = inputs.loginUser?.value;
|
||||
const p = inputs.loginPass?.value;
|
||||
if (!u || !p)
|
||||
return showError(displays.loginError, "Please fill all fields");
|
||||
|
||||
clearErrors();
|
||||
let hasError = false;
|
||||
if (!u) {
|
||||
showError(displays.loginUserError, "Username required");
|
||||
hasError = true;
|
||||
}
|
||||
if (!p) {
|
||||
showError(displays.loginPassError, "Password required");
|
||||
hasError = true;
|
||||
}
|
||||
if (hasError) return;
|
||||
|
||||
setLoading("btn-login-submit", true, "Login");
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_API_BASE}/login`, {
|
||||
method: "POST",
|
||||
@@ -129,18 +161,23 @@ const { titleSvg } = Astro.props;
|
||||
if (inputs.loginUser) inputs.loginUser.value = "";
|
||||
if (inputs.loginPass) inputs.loginPass.value = "";
|
||||
} else {
|
||||
showError(displays.loginError, data.error || "Login failed");
|
||||
showError(displays.loginPassError, data.error || "Login failed");
|
||||
}
|
||||
} catch (e) {
|
||||
showError(displays.loginError, "Network error");
|
||||
showError(displays.loginPassError, "Network error");
|
||||
} finally {
|
||||
setLoading("btn-login-submit", false, "Login");
|
||||
}
|
||||
}
|
||||
|
||||
async function startQQAuth() {
|
||||
const qq = inputs.qqId?.value;
|
||||
if (!qq) return showError(displays.qqError, "Enter your QQ");
|
||||
|
||||
clearErrors();
|
||||
if (!qq) return showError(displays.qqError, "Enter your QQ");
|
||||
if (!/^\d{5,11}$/.test(qq))
|
||||
return showError(displays.qqError, "QQ number must be 5-11 digits");
|
||||
|
||||
setLoading("btn-qq-next", true, "Next");
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_API_BASE}/auth/qq/start`, {
|
||||
method: "POST",
|
||||
@@ -149,7 +186,8 @@ const { titleSvg } = Astro.props;
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code) {
|
||||
if (displays.verifyCode) displays.verifyCode.textContent = data.code;
|
||||
if (displays.verifyCode)
|
||||
displays.verifyCode.textContent = `/login ${data.code}`;
|
||||
currentQQ = qq;
|
||||
switchView("verification");
|
||||
startPolling(qq);
|
||||
@@ -158,6 +196,8 @@ const { titleSvg } = Astro.props;
|
||||
}
|
||||
} catch (e) {
|
||||
showError(displays.qqError, "Network error");
|
||||
} finally {
|
||||
setLoading("btn-qq-next", false, "Next");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,9 +228,20 @@ const { titleSvg } = Astro.props;
|
||||
async function performRegister() {
|
||||
const u = inputs.regUser?.value;
|
||||
const p = inputs.regPass?.value;
|
||||
if (!u || !p) return showError(displays.regError, "Please fill all fields");
|
||||
|
||||
clearErrors();
|
||||
let hasError = false;
|
||||
if (!u) {
|
||||
showError(displays.regUserError, "Username required");
|
||||
hasError = true;
|
||||
}
|
||||
if (!p) {
|
||||
showError(displays.regPassError, "Password required");
|
||||
hasError = true;
|
||||
}
|
||||
if (hasError) return;
|
||||
|
||||
setLoading("btn-register-submit", true, "Register");
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_API_BASE}/register`, {
|
||||
method: "POST",
|
||||
@@ -211,10 +262,12 @@ const { titleSvg } = Astro.props;
|
||||
switchView("login");
|
||||
}
|
||||
} else {
|
||||
showError(displays.regError, data.error || "Registration failed");
|
||||
showError(displays.regPassError, data.error || "Registration failed");
|
||||
}
|
||||
} catch (e) {
|
||||
showError(displays.regError, "Network error");
|
||||
showError(displays.regPassError, "Network error");
|
||||
} finally {
|
||||
setLoading("btn-register-submit", false, "Register");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,9 +319,20 @@ const { titleSvg } = Astro.props;
|
||||
document
|
||||
.getElementById("btn-qq-mode")
|
||||
?.addEventListener("click", () => switchView("qqInput"));
|
||||
document
|
||||
.getElementById("btn-qq-next")
|
||||
?.addEventListener("click", startQQAuth);
|
||||
const btnQQNext = document.getElementById("btn-qq-next") as HTMLButtonElement;
|
||||
if (btnQQNext) {
|
||||
// Initial state
|
||||
btnQQNext.disabled = true;
|
||||
btnQQNext.addEventListener("click", startQQAuth);
|
||||
}
|
||||
|
||||
if (inputs.qqId) {
|
||||
inputs.qqId.addEventListener("input", () => {
|
||||
const val = inputs.qqId.value;
|
||||
const isValid = /^\d{5,11}$/.test(val);
|
||||
if (btnQQNext) btnQQNext.disabled = !isValid;
|
||||
});
|
||||
}
|
||||
document
|
||||
.getElementById("btn-cancel-verify")
|
||||
?.addEventListener("click", () => {
|
||||
@@ -317,6 +381,25 @@ const { titleSvg } = Astro.props;
|
||||
checkLoginState();
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
.btn-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.island-container {
|
||||
display: grid;
|
||||
|
||||
@@ -20,18 +20,26 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
|
||||
</svg>
|
||||
</button>
|
||||
<h2>Login</h2>
|
||||
<input
|
||||
type="text"
|
||||
id="login-username"
|
||||
placeholder="Username"
|
||||
class="input-field"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="login-password"
|
||||
placeholder="Password"
|
||||
class="input-field"
|
||||
>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="login-username"
|
||||
placeholder="Username"
|
||||
class="input-field"
|
||||
/>
|
||||
<div id="login-username-error" class="input-error hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
id="login-password"
|
||||
placeholder="Password"
|
||||
class="input-field"
|
||||
/>
|
||||
<div id="login-password-error" class="input-error hidden"></div>
|
||||
</div>
|
||||
|
||||
<button id="btn-login-submit" class="submit-btn">Login</button>
|
||||
|
||||
<div class="divider">
|
||||
@@ -39,9 +47,8 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
|
||||
</div>
|
||||
|
||||
<button id="btn-qq-mode" class="qq-btn" title="Login with QQ">
|
||||
<span set:html={qqIconRaw} style="display: flex; align-items: center;"/>
|
||||
<span set:html={qqIconRaw} style="display: flex; align-items: center;" />
|
||||
</button>
|
||||
<p class="error-msg hidden" id="login-error"></p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -99,6 +106,11 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.input-group {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1rem;
|
||||
@@ -111,7 +123,7 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background 0.2s;
|
||||
margin-bottom: 1rem;
|
||||
/* margin-bottom removed */
|
||||
font-family:
|
||||
monospace, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New";
|
||||
@@ -125,6 +137,20 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Specific styles for input errors */
|
||||
.input-error {
|
||||
color: #ff3333; /* Stronger red */
|
||||
font-size: 0.85rem;
|
||||
margin-top: 6px; /* Space between input and error */
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
/* Remove invalid style if present */
|
||||
.error-msg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
@@ -206,11 +232,6 @@ import qqIconRaw from "../../assets/icons/qq.svg?raw";
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.error-msg {
|
||||
color: #ff6b6b;
|
||||
margin-top: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,20 @@
|
||||
</button>
|
||||
<h2>QQ Login</h2>
|
||||
<p class="instruction">Enter your QQ id to start</p>
|
||||
<input type="text" id="qq-id-input" placeholder="QQ" class="input-field">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="qq-id-input"
|
||||
placeholder="QQ"
|
||||
class="input-field"
|
||||
minlength="5"
|
||||
maxlength="11"
|
||||
/>
|
||||
<div id="qq-id-error" class="input-error hidden"></div>
|
||||
</div>
|
||||
|
||||
<button id="btn-qq-next" class="submit-btn">Next</button>
|
||||
<button class="back-text-btn" data-target="view-login">Back to Login</button>
|
||||
<p class="error-msg hidden" id="qq-error"></p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -84,6 +94,11 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1rem;
|
||||
@@ -96,7 +111,7 @@
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background 0.2s;
|
||||
margin-bottom: 1rem;
|
||||
/* margin-bottom removed */
|
||||
font-family:
|
||||
monospace, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New";
|
||||
@@ -110,6 +125,20 @@
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Specific styles for input errors */
|
||||
.input-error {
|
||||
color: #ff3333; /* Stronger red */
|
||||
font-size: 0.85rem;
|
||||
margin-top: 6px; /* Space between input and error */
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
/* Remove invalid style if present */
|
||||
.error-msg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
@@ -153,12 +182,6 @@
|
||||
.back-text-btn:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
color: #ff6b6b;
|
||||
margin-top: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
}
|
||||
|
||||
@@ -17,20 +17,27 @@
|
||||
</button>
|
||||
<h2>Complete Registration</h2>
|
||||
<p class="instruction">Set up your username and password</p>
|
||||
<input
|
||||
type="text"
|
||||
id="reg-username"
|
||||
placeholder="Username"
|
||||
class="input-field"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
id="reg-password"
|
||||
placeholder="Password"
|
||||
class="input-field"
|
||||
>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="reg-username"
|
||||
placeholder="Username"
|
||||
class="input-field"
|
||||
/>
|
||||
<div id="reg-username-error" class="input-error hidden"></div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="password"
|
||||
id="reg-password"
|
||||
placeholder="Password"
|
||||
class="input-field"
|
||||
/>
|
||||
<div id="reg-password-error" class="input-error hidden"></div>
|
||||
</div>
|
||||
|
||||
<button id="btn-register-submit" class="submit-btn">Register</button>
|
||||
<p class="error-msg hidden" id="reg-error"></p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -94,6 +101,11 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1rem;
|
||||
@@ -106,7 +118,7 @@
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
background 0.2s;
|
||||
margin-bottom: 1rem;
|
||||
/* margin-bottom removed */
|
||||
font-family:
|
||||
monospace, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New";
|
||||
@@ -120,6 +132,20 @@
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Specific styles for input errors */
|
||||
.input-error {
|
||||
color: #ff3333; /* Stronger red */
|
||||
font-size: 0.85rem;
|
||||
margin-top: 6px; /* Space between input and error */
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
/* Remove invalid style if present */
|
||||
.error-msg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
@@ -151,12 +177,6 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
color: #ff6b6b;
|
||||
margin-top: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
}
|
||||
|
||||
@@ -94,8 +94,33 @@
|
||||
border-radius: 12px;
|
||||
border: 1px dashed rgba(255, 255, 255, 0.3);
|
||||
margin: 1rem 0;
|
||||
user-select: all;
|
||||
user-select: none;
|
||||
color: #7cfbff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shake {
|
||||
animation: shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%,
|
||||
90% {
|
||||
transform: translate3d(-1px, 0, 0);
|
||||
}
|
||||
20%,
|
||||
80% {
|
||||
transform: translate3d(2px, 0, 0);
|
||||
}
|
||||
30%,
|
||||
50%,
|
||||
70% {
|
||||
transform: translate3d(-4px, 0, 0);
|
||||
}
|
||||
40%,
|
||||
60% {
|
||||
transform: translate3d(4px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
@@ -143,3 +168,38 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const codeDisplay = document.querySelector(
|
||||
"#verification-code-display",
|
||||
) as HTMLElement;
|
||||
const statusText = document.querySelector(".status-text") as HTMLElement;
|
||||
|
||||
if (codeDisplay && statusText) {
|
||||
codeDisplay.addEventListener("click", async () => {
|
||||
const code = codeDisplay.innerText;
|
||||
if (!code || code === "......") return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
|
||||
// Visual feedback
|
||||
const originalStatus = statusText.innerText;
|
||||
statusText.innerText = "Copied to clipboard!";
|
||||
statusText.style.color = "#7cfbff";
|
||||
|
||||
codeDisplay.classList.remove("shake");
|
||||
// Trigger reflow
|
||||
void codeDisplay.offsetWidth;
|
||||
codeDisplay.classList.add("shake");
|
||||
|
||||
setTimeout(() => {
|
||||
statusText.innerText = originalStatus;
|
||||
statusText.style.color = "";
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user