mirror of
https://github.com/101island/lolisland.us.git
synced 2026-03-01 11:49:43 +08:00
feat: add avatar upload button to dashboard
This commit is contained in:
@@ -45,6 +45,7 @@ const { titleSvg } = Astro.props;
|
|||||||
// Actually, let's keep state here.
|
// Actually, let's keep state here.
|
||||||
let isLoggedIn = false;
|
let isLoggedIn = false;
|
||||||
let currentQQ = "";
|
let currentQQ = "";
|
||||||
|
let currentUser = "";
|
||||||
let isPolling = false;
|
let isPolling = false;
|
||||||
|
|
||||||
// DOM Helpers (Dynamic)
|
// DOM Helpers (Dynamic)
|
||||||
@@ -128,7 +129,21 @@ const { titleSvg } = Astro.props;
|
|||||||
function updateDashboard() {
|
function updateDashboard() {
|
||||||
const displays = getDisplays();
|
const displays = getDisplays();
|
||||||
if (displays.dashAvatar) {
|
if (displays.dashAvatar) {
|
||||||
if (currentQQ) {
|
if (currentUser) {
|
||||||
|
displays.dashAvatar.src = `${BACKEND_API_BASE}/user/avatar/${currentUser}`;
|
||||||
|
// Add onerror fallback to QQ or default
|
||||||
|
displays.dashAvatar.onerror = () => {
|
||||||
|
if (currentQQ) {
|
||||||
|
displays.dashAvatar.src = `https://q.qlogo.cn/headimg_dl?dst_uin=${currentQQ}&spec=640&img_type=jpg`;
|
||||||
|
} else {
|
||||||
|
displays.dashAvatar.src =
|
||||||
|
"https://ui-avatars.com/api/?name=" +
|
||||||
|
(currentUser || "User") +
|
||||||
|
"&background=random";
|
||||||
|
}
|
||||||
|
displays.dashAvatar.onerror = null; // Prevent infinite loop
|
||||||
|
};
|
||||||
|
} else if (currentQQ) {
|
||||||
displays.dashAvatar.src = `https://q.qlogo.cn/headimg_dl?dst_uin=${currentQQ}&spec=640&img_type=jpg`;
|
displays.dashAvatar.src = `https://q.qlogo.cn/headimg_dl?dst_uin=${currentQQ}&spec=640&img_type=jpg`;
|
||||||
} else {
|
} else {
|
||||||
displays.dashAvatar.src =
|
displays.dashAvatar.src =
|
||||||
@@ -139,6 +154,18 @@ const { titleSvg } = Astro.props;
|
|||||||
|
|
||||||
function handleLoginSuccess(token: string, qq: string) {
|
function handleLoginSuccess(token: string, qq: string) {
|
||||||
localStorage.setItem("token", token);
|
localStorage.setItem("token", token);
|
||||||
|
|
||||||
|
// Extract username from token
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||||
|
if (payload.user_id) {
|
||||||
|
currentUser = payload.user_id;
|
||||||
|
localStorage.setItem("username", currentUser);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to decode token", e);
|
||||||
|
}
|
||||||
|
|
||||||
if (qq) {
|
if (qq) {
|
||||||
localStorage.setItem("qq", String(qq));
|
localStorage.setItem("qq", String(qq));
|
||||||
currentQQ = String(qq);
|
currentQQ = String(qq);
|
||||||
@@ -332,8 +359,10 @@ const { titleSvg } = Astro.props;
|
|||||||
function checkLoginState() {
|
function checkLoginState() {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
const storedQQ = localStorage.getItem("qq");
|
const storedQQ = localStorage.getItem("qq");
|
||||||
|
const storedUser = localStorage.getItem("username");
|
||||||
isLoggedIn = !!token;
|
isLoggedIn = !!token;
|
||||||
if (storedQQ) currentQQ = storedQQ;
|
if (storedQQ) currentQQ = storedQQ;
|
||||||
|
if (storedUser) currentUser = storedUser;
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
updateDashboard();
|
updateDashboard();
|
||||||
|
|||||||
@@ -25,11 +25,110 @@ const t = getTranslations(lang);
|
|||||||
<div class="dashboard-avatar">
|
<div class="dashboard-avatar">
|
||||||
<img id="dashboard-avatar-img" src="" alt="User Avatar">
|
<img id="dashboard-avatar-img" src="" alt="User Avatar">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="file" id="avatar-upload-input" accept="image/*" class="hidden">
|
||||||
|
<button id="btn-change-avatar" class="change-avatar-btn">
|
||||||
|
{t("dashboard.change_avatar")}
|
||||||
|
</button>
|
||||||
|
|
||||||
<h2>{t("dashboard.logged_in")}</h2>
|
<h2>{t("dashboard.logged_in")}</h2>
|
||||||
<p class="instruction">{t("dashboard.instruction")}</p>
|
<p class="instruction">{t("dashboard.instruction")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { BACKEND_API_BASE } from "../../config/loginApiBaseUrl";
|
||||||
|
|
||||||
|
document.addEventListener("astro:page-load", () => {
|
||||||
|
const btnChange = document.getElementById("btn-change-avatar");
|
||||||
|
const input = document.getElementById(
|
||||||
|
"avatar-upload-input",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const img = document.getElementById(
|
||||||
|
"dashboard-avatar-img",
|
||||||
|
) as HTMLImageElement;
|
||||||
|
|
||||||
|
if (btnChange && input) {
|
||||||
|
btnChange.addEventListener("click", () => {
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener("change", async () => {
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Reset input value so same file can be selected again if needed
|
||||||
|
// but proceed first.
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("avatar", file);
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const originalText = btnChange.textContent;
|
||||||
|
btnChange.textContent = "...";
|
||||||
|
(btnChange as HTMLButtonElement).disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BACKEND_API_BASE}/user/avatar`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
// Update image source to force reload
|
||||||
|
if (img) {
|
||||||
|
const currentSrc = img.src.split("?")[0];
|
||||||
|
img.src = `${currentSrc}?t=${Date.now()}`;
|
||||||
|
}
|
||||||
|
// Dispatch event for other components (like navbar) if they listen
|
||||||
|
window.dispatchEvent(new CustomEvent("login-success"));
|
||||||
|
} else {
|
||||||
|
alert("Upload failed");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Avatar upload failed", e);
|
||||||
|
alert("Upload failed");
|
||||||
|
} finally {
|
||||||
|
btnChange.textContent = originalText;
|
||||||
|
(btnChange as HTMLButtonElement).disabled = false;
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.change-avatar-btn {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-avatar-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-avatar-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
/* biome-ignore lint/complexity/noImportantStyles: utility class */
|
/* biome-ignore lint/complexity/noImportantStyles: utility class */
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -99,12 +198,12 @@ const t = getTranslations(lang);
|
|||||||
padding-bottom: 3rem;
|
padding-bottom: 3rem;
|
||||||
}
|
}
|
||||||
.dashboard-avatar {
|
.dashboard-avatar {
|
||||||
width: 96px;
|
width: 128px;
|
||||||
height: 96px;
|
height: 128px;
|
||||||
border-radius: 24px;
|
border-radius: 32px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
.dashboard-avatar img {
|
.dashboard-avatar img {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const ui = {
|
|||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
"dashboard.logged_in": "Logged In",
|
"dashboard.logged_in": "Logged In",
|
||||||
|
"dashboard.change_avatar": "Change Avatar",
|
||||||
"dashboard.instruction": "Development in progress...",
|
"dashboard.instruction": "Development in progress...",
|
||||||
|
|
||||||
// Verification
|
// Verification
|
||||||
@@ -98,6 +99,7 @@ export const ui = {
|
|||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
"dashboard.logged_in": "已登录",
|
"dashboard.logged_in": "已登录",
|
||||||
|
"dashboard.change_avatar": "修改头像",
|
||||||
"dashboard.instruction": "开发中...",
|
"dashboard.instruction": "开发中...",
|
||||||
|
|
||||||
// Verification
|
// Verification
|
||||||
@@ -152,6 +154,7 @@ export const ui = {
|
|||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
"dashboard.logged_in": "已登入",
|
"dashboard.logged_in": "已登入",
|
||||||
|
"dashboard.change_avatar": "修改頭像",
|
||||||
"dashboard.instruction": "開發中...",
|
"dashboard.instruction": "開發中...",
|
||||||
|
|
||||||
// Verification
|
// Verification
|
||||||
@@ -206,6 +209,7 @@ export const ui = {
|
|||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
"dashboard.logged_in": "ログイン中",
|
"dashboard.logged_in": "ログイン中",
|
||||||
|
"dashboard.change_avatar": "アバター変更",
|
||||||
"dashboard.instruction": "開発中...",
|
"dashboard.instruction": "開発中...",
|
||||||
|
|
||||||
// Verification
|
// Verification
|
||||||
|
|||||||
Reference in New Issue
Block a user