Wire islands API and avatar uploads
This commit is contained in:
parent
e1e5999617
commit
d06819181e
|
|
@ -296,109 +296,6 @@ export async function pollNewPosts(params = {}) {
|
||||||
* Replace with real API call when island-service is ready
|
* Replace with real API call when island-service is ready
|
||||||
*/
|
*/
|
||||||
export async function fetchIslands(params = {}) {
|
export async function fetchIslands(params = {}) {
|
||||||
// MOCK DATA - Remove this and uncomment API call when backend is ready
|
|
||||||
console.log('[fetchIslands] Using MOCK data', params);
|
|
||||||
|
|
||||||
// Simulate network delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
|
||||||
|
|
||||||
const mockIslands = [
|
|
||||||
{
|
|
||||||
id: 'island-1',
|
|
||||||
user_id: 'user-alice',
|
|
||||||
username: 'alice',
|
|
||||||
seed: 12345678,
|
|
||||||
virtual_x: -73.5,
|
|
||||||
virtual_y: 45.5,
|
|
||||||
virtual_z: 0,
|
|
||||||
terrain_type: 'tropical',
|
|
||||||
color_palette: 'vibrant',
|
|
||||||
size: 'medium',
|
|
||||||
display_name: "Alice's Paradise",
|
|
||||||
bio: 'Welcome to my tropical island! 🌴',
|
|
||||||
avatar_url: null,
|
|
||||||
content_count: 12,
|
|
||||||
visitors: 234,
|
|
||||||
created_at: '2025-01-15T10:00:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'island-2',
|
|
||||||
user_id: 'user-bob',
|
|
||||||
username: 'bob',
|
|
||||||
seed: 87654321,
|
|
||||||
virtual_x: -74.0,
|
|
||||||
virtual_y: 45.8,
|
|
||||||
virtual_z: 0,
|
|
||||||
terrain_type: 'desert',
|
|
||||||
color_palette: 'sunset',
|
|
||||||
size: 'large',
|
|
||||||
display_name: "Bob's Oasis",
|
|
||||||
bio: 'Desert vibes only ☀️',
|
|
||||||
avatar_url: null,
|
|
||||||
content_count: 8,
|
|
||||||
visitors: 156,
|
|
||||||
created_at: '2025-01-10T14:30:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'island-3',
|
|
||||||
user_id: 'user-charlie',
|
|
||||||
username: 'charlie',
|
|
||||||
seed: 11223344,
|
|
||||||
virtual_x: -73.2,
|
|
||||||
virtual_y: 45.3,
|
|
||||||
virtual_z: 0,
|
|
||||||
terrain_type: 'arctic',
|
|
||||||
color_palette: 'monochrome',
|
|
||||||
size: 'small',
|
|
||||||
display_name: "Charlie's Iceberg",
|
|
||||||
bio: 'Chill zone ❄️',
|
|
||||||
avatar_url: null,
|
|
||||||
content_count: 5,
|
|
||||||
visitors: 89,
|
|
||||||
created_at: '2025-01-20T09:15:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'island-4',
|
|
||||||
user_id: 'user-diana',
|
|
||||||
username: 'diana',
|
|
||||||
seed: 99887766,
|
|
||||||
virtual_x: -73.8,
|
|
||||||
virtual_y: 45.6,
|
|
||||||
virtual_z: 0,
|
|
||||||
terrain_type: 'volcanic',
|
|
||||||
color_palette: 'vibrant',
|
|
||||||
size: 'medium',
|
|
||||||
display_name: "Diana's Volcano",
|
|
||||||
bio: 'Hot takes and lava cakes 🌋',
|
|
||||||
avatar_url: null,
|
|
||||||
content_count: 15,
|
|
||||||
visitors: 312,
|
|
||||||
created_at: '2025-01-12T16:45:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'island-5',
|
|
||||||
user_id: 'user-eve',
|
|
||||||
username: 'eve',
|
|
||||||
seed: 55443322,
|
|
||||||
virtual_x: -74.2,
|
|
||||||
virtual_y: 45.4,
|
|
||||||
virtual_z: 0,
|
|
||||||
terrain_type: 'meadow',
|
|
||||||
color_palette: 'pastel',
|
|
||||||
size: 'large',
|
|
||||||
display_name: "Eve's Garden",
|
|
||||||
bio: 'Peace, love, and flowers 🌸',
|
|
||||||
avatar_url: null,
|
|
||||||
content_count: 20,
|
|
||||||
visitors: 445,
|
|
||||||
created_at: '2025-01-08T11:20:00Z'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return mockIslands;
|
|
||||||
|
|
||||||
/*
|
|
||||||
// REAL API CALL - Uncomment when island-service is ready:
|
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
|
|
||||||
if (typeof params.lat === "number") qs.set("lat", String(params.lat));
|
if (typeof params.lat === "number") qs.set("lat", String(params.lat));
|
||||||
|
|
@ -406,7 +303,7 @@ export async function fetchIslands(params = {}) {
|
||||||
if (typeof params.radius_km === "number") qs.set("radius_km", String(params.radius_km));
|
if (typeof params.radius_km === "number") qs.set("radius_km", String(params.radius_km));
|
||||||
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
if (typeof params.limit === "number") qs.set("limit", String(params.limit));
|
||||||
|
|
||||||
const url = `http://localhost:8090/api/islands/nearby?${qs.toString()}`;
|
const url = `${API_BASE}/islands/nearby?${qs.toString()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { credentials: "include" });
|
const res = await fetch(url, { credentials: "include" });
|
||||||
|
|
@ -420,7 +317,6 @@ export async function fetchIslands(params = {}) {
|
||||||
console.warn("fetchIslands error:", err);
|
console.warn("fetchIslands error:", err);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -430,108 +326,8 @@ export async function fetchIslands(params = {}) {
|
||||||
* NOTE: Currently using MOCK DATA for testing
|
* NOTE: Currently using MOCK DATA for testing
|
||||||
*/
|
*/
|
||||||
export async function fetchIsland(islandId) {
|
export async function fetchIsland(islandId) {
|
||||||
// MOCK DATA - Remove this and uncomment API call when backend is ready
|
if (!islandId) return null;
|
||||||
console.log('[fetchIsland] Using MOCK data for island:', islandId);
|
const url = `${API_BASE}/islands/${encodeURIComponent(islandId)}`;
|
||||||
|
|
||||||
// Simulate network delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
|
|
||||||
// Mock island with content
|
|
||||||
const mockIslandData = {
|
|
||||||
'island-1': {
|
|
||||||
id: 'island-1',
|
|
||||||
user_id: 'user-alice',
|
|
||||||
username: 'alice',
|
|
||||||
seed: 12345678,
|
|
||||||
virtual_x: -73.5,
|
|
||||||
virtual_y: 45.5,
|
|
||||||
virtual_z: 0,
|
|
||||||
terrain_type: 'tropical',
|
|
||||||
color_palette: 'vibrant',
|
|
||||||
size: 'medium',
|
|
||||||
display_name: "Alice's Paradise",
|
|
||||||
bio: 'Welcome to my tropical island! 🌴',
|
|
||||||
avatar_url: null,
|
|
||||||
content_count: 3,
|
|
||||||
visitors: 234,
|
|
||||||
created_at: '2025-01-15T10:00:00Z',
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
id: 'content-1',
|
|
||||||
island_id: 'island-1',
|
|
||||||
content_type: 'post',
|
|
||||||
content_id: 'post-123',
|
|
||||||
position_x: 0.3,
|
|
||||||
position_y: 0.7,
|
|
||||||
position_z: 0.5,
|
|
||||||
scale: 1.0,
|
|
||||||
rotation: 0,
|
|
||||||
created_at: '2025-01-16T10:00:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'content-2',
|
|
||||||
island_id: 'island-1',
|
|
||||||
content_type: 'app',
|
|
||||||
content_id: 'app-456',
|
|
||||||
position_x: 0.6,
|
|
||||||
position_y: 0.4,
|
|
||||||
position_z: 0.5,
|
|
||||||
scale: 1.0,
|
|
||||||
rotation: 45,
|
|
||||||
created_at: '2025-01-17T14:30:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'content-3',
|
|
||||||
island_id: 'island-1',
|
|
||||||
content_type: 'post',
|
|
||||||
content_id: 'post-789',
|
|
||||||
position_x: 0.8,
|
|
||||||
position_y: 0.8,
|
|
||||||
position_z: 0.5,
|
|
||||||
scale: 1.0,
|
|
||||||
rotation: 90,
|
|
||||||
created_at: '2025-01-18T09:15:00Z'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'island-2': {
|
|
||||||
id: 'island-2',
|
|
||||||
user_id: 'user-bob',
|
|
||||||
username: 'bob',
|
|
||||||
seed: 87654321,
|
|
||||||
virtual_x: -74.0,
|
|
||||||
virtual_y: 45.8,
|
|
||||||
terrain_type: 'desert',
|
|
||||||
color_palette: 'sunset',
|
|
||||||
size: 'large',
|
|
||||||
display_name: "Bob's Oasis",
|
|
||||||
bio: 'Desert vibes only ☀️',
|
|
||||||
content_count: 2,
|
|
||||||
visitors: 156,
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
id: 'content-4',
|
|
||||||
content_type: 'post',
|
|
||||||
position_x: 0.5,
|
|
||||||
position_y: 0.5,
|
|
||||||
position_z: 0.5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'content-5',
|
|
||||||
content_type: 'document',
|
|
||||||
position_x: 0.2,
|
|
||||||
position_y: 0.3,
|
|
||||||
position_z: 0.5
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return mockIslandData[islandId] || null;
|
|
||||||
|
|
||||||
/*
|
|
||||||
// REAL API CALL - Uncomment when island-service is ready:
|
|
||||||
const url = `http://localhost:8090/api/islands/${islandId}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { credentials: "include" });
|
const res = await fetch(url, { credentials: "include" });
|
||||||
|
|
@ -544,7 +340,45 @@ export async function fetchIsland(islandId) {
|
||||||
console.warn("fetchIsland error:", err);
|
console.warn("fetchIsland error:", err);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
export async function fetchIslandByUsername(username) {
|
||||||
|
if (!username) return null;
|
||||||
|
const url = `${API_BASE}/islands/by-username?username=${encodeURIComponent(username)}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { credentials: "include" });
|
||||||
|
if (!res.ok) {
|
||||||
|
console.warn("fetchIslandByUsername failed", res.status);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("fetchIslandByUsername error:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadProfileAvatar(file) {
|
||||||
|
if (!file) return { ok: false };
|
||||||
|
const url = `${API_BASE}/profile/avatar`;
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("file", file);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
credentials: "include",
|
||||||
|
headers: { ...authHeaders() },
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => "");
|
||||||
|
return { ok: false, status: res.status, text };
|
||||||
|
}
|
||||||
|
return await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("uploadProfileAvatar error:", err);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function recordPostView(postId) {
|
export async function recordPostView(postId) {
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ async function me(accessToken) {
|
||||||
json,
|
json,
|
||||||
text,
|
text,
|
||||||
emailVerified,
|
emailVerified,
|
||||||
|
avatarUrl: u && typeof u.avatar_url === "string" ? u.avatar_url : "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,6 +183,7 @@ export function AuthProvider({ children }) {
|
||||||
const [status, setStatus] = useState("loading"); // loading | anon | auth
|
const [status, setStatus] = useState("loading"); // loading | anon | auth
|
||||||
const [user, setUser] = useState(null); // { username }
|
const [user, setUser] = useState(null); // { username }
|
||||||
const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... }
|
const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... }
|
||||||
|
const [avatarUrl, setAvatarUrl] = useState("");
|
||||||
|
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [lastInfo, setLastInfo] = useState("");
|
const [lastInfo, setLastInfo] = useState("");
|
||||||
|
|
@ -194,6 +196,7 @@ export function AuthProvider({ children }) {
|
||||||
setStatus("anon");
|
setStatus("anon");
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setTokens(null);
|
setTokens(null);
|
||||||
|
setAvatarUrl("");
|
||||||
setError(msg || "");
|
setError(msg || "");
|
||||||
setLastInfo("");
|
setLastInfo("");
|
||||||
setNeedsEmailVerification(false);
|
setNeedsEmailVerification(false);
|
||||||
|
|
@ -218,6 +221,9 @@ export function AuthProvider({ children }) {
|
||||||
// /api/me is the truth; token claim is fallback only
|
// /api/me is the truth; token claim is fallback only
|
||||||
const mr = await me(at);
|
const mr = await me(at);
|
||||||
let verified = mr.emailVerified
|
let verified = mr.emailVerified
|
||||||
|
if (mr.avatarUrl) {
|
||||||
|
setAvatarUrl(mr.avatarUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof verified !== "boolean") {
|
if (typeof verified !== "boolean") {
|
||||||
const tv = tokenEmailVerified(at);
|
const tv = tokenEmailVerified(at);
|
||||||
|
|
@ -553,6 +559,7 @@ export function AuthProvider({ children }) {
|
||||||
user,
|
user,
|
||||||
tokens,
|
tokens,
|
||||||
error,
|
error,
|
||||||
|
avatarUrl,
|
||||||
|
|
||||||
// expected by UI
|
// expected by UI
|
||||||
loading,
|
loading,
|
||||||
|
|
@ -571,13 +578,14 @@ export function AuthProvider({ children }) {
|
||||||
logout,
|
logout,
|
||||||
restore,
|
restore,
|
||||||
refreshProfile,
|
refreshProfile,
|
||||||
|
setAvatarUrl,
|
||||||
setError,
|
setError,
|
||||||
setLastInfo,
|
setLastInfo,
|
||||||
setNeedsEmailVerification,
|
setNeedsEmailVerification,
|
||||||
clearError: () => setError(""),
|
clearError: () => setError(""),
|
||||||
clearInfo: () => setLastInfo(""),
|
clearInfo: () => setLastInfo(""),
|
||||||
};
|
};
|
||||||
}, [status, user, tokens, error, lastInfo, needsEmailVerification]);
|
}, [status, user, tokens, error, lastInfo, needsEmailVerification, avatarUrl]);
|
||||||
|
|
||||||
return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
|
return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick }
|
||||||
logout,
|
logout,
|
||||||
loginWithProvider,
|
loginWithProvider,
|
||||||
ssoEnabled,
|
ssoEnabled,
|
||||||
|
avatarUrl,
|
||||||
needsEmailVerification,
|
needsEmailVerification,
|
||||||
lastError,
|
lastError,
|
||||||
lastInfo,
|
lastInfo,
|
||||||
|
|
@ -317,7 +318,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick }
|
||||||
{authenticated ? (
|
{authenticated ? (
|
||||||
<div className="sw-userchip" title={username}>
|
<div className="sw-userchip" title={username}>
|
||||||
<div className="sw-avatar">
|
<div className="sw-avatar">
|
||||||
<span>{initialLetter(username)}</span>
|
{avatarUrl ? <img src={avatarUrl} alt={username} /> : <span>{initialLetter(username)}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="sw-usertext">
|
<div className="sw-usertext">
|
||||||
<div className="sw-userline">Online</div>
|
<div className="sw-userline">Online</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { fetchIsland } from '../../api/client';
|
import { fetchIslandByUsername, uploadProfileAvatar } from '../../api/client';
|
||||||
|
import { useAuth } from '../../auth/AuthContext';
|
||||||
import '../../styles/profile.css';
|
import '../../styles/profile.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,37 +12,26 @@ import '../../styles/profile.css';
|
||||||
* - onVisitIsland: function(island) - called when "Visit Island" is clicked
|
* - onVisitIsland: function(island) - called when "Visit Island" is clicked
|
||||||
*/
|
*/
|
||||||
export default function UserProfile({ username, onClose, onVisitIsland }) {
|
export default function UserProfile({ username, onClose, onVisitIsland }) {
|
||||||
|
const { authenticated, username: authedUser, setAvatarUrl } = useAuth();
|
||||||
const [island, setIsland] = useState(null);
|
const [island, setIsland] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const fileRef = useRef(null);
|
||||||
|
|
||||||
|
const isSelf = authenticated && authedUser && authedUser.toLowerCase() === username.toLowerCase();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!username) return;
|
if (!username) return;
|
||||||
|
|
||||||
// Find island by username (mock data uses island-{number})
|
fetchIslandByUsername(username)
|
||||||
// In real backend, we'd call /api/islands/by-username/{username}
|
.then(data => {
|
||||||
const mockIslandMap = {
|
setIsland(data);
|
||||||
'alice': 'island-1',
|
setLoading(false);
|
||||||
'bob': 'island-2',
|
})
|
||||||
'charlie': 'island-3',
|
.catch(err => {
|
||||||
'diana': 'island-4',
|
console.error('[UserProfile] Error fetching island:', err);
|
||||||
'eve': 'island-5',
|
setLoading(false);
|
||||||
};
|
});
|
||||||
|
|
||||||
const islandId = mockIslandMap[username.toLowerCase()];
|
|
||||||
|
|
||||||
if (islandId) {
|
|
||||||
fetchIsland(islandId)
|
|
||||||
.then(data => {
|
|
||||||
setIsland(data);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('[UserProfile] Error fetching island:', err);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [username]);
|
}, [username]);
|
||||||
|
|
||||||
const handleVisitIsland = () => {
|
const handleVisitIsland = () => {
|
||||||
|
|
@ -50,6 +40,22 @@ export default function UserProfile({ username, onClose, onVisitIsland }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChooseAvatar = () => {
|
||||||
|
if (fileRef.current) fileRef.current.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarChange = async (e) => {
|
||||||
|
const file = e.target.files && e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
setUploading(true);
|
||||||
|
const res = await uploadProfileAvatar(file);
|
||||||
|
if (res && res.ok && res.avatar_url) {
|
||||||
|
setIsland(prev => (prev ? { ...prev, avatar_url: res.avatar_url } : prev));
|
||||||
|
if (setAvatarUrl) setAvatarUrl(res.avatar_url);
|
||||||
|
}
|
||||||
|
setUploading(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user-profile-modal" onClick={onClose}>
|
<div className="user-profile-modal" onClick={onClose}>
|
||||||
<div className="user-profile-container" onClick={(e) => e.stopPropagation()}>
|
<div className="user-profile-container" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|
@ -69,6 +75,25 @@ export default function UserProfile({ username, onClose, onVisitIsland }) {
|
||||||
{island?.display_name && island.display_name !== `${username}'s Island` && (
|
{island?.display_name && island.display_name !== `${username}'s Island` && (
|
||||||
<p className="user-profile-display-name">{island.display_name}</p>
|
<p className="user-profile-display-name">{island.display_name}</p>
|
||||||
)}
|
)}
|
||||||
|
{isSelf ? (
|
||||||
|
<div className="user-profile-avatar-actions">
|
||||||
|
<button
|
||||||
|
className="user-profile-btn user-profile-btn-ghost"
|
||||||
|
type="button"
|
||||||
|
disabled={uploading}
|
||||||
|
onClick={handleChooseAvatar}
|
||||||
|
>
|
||||||
|
{uploading ? 'Uploading...' : 'Change photo'}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={fileRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleAvatarChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,24 @@
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-profile-avatar-actions{
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
.user-profile-btn-ghost{
|
||||||
|
background: rgba(15, 23, 42, 0.2);
|
||||||
|
color: #e2e8f0;
|
||||||
|
border: 1px solid rgba(148,163,184,0.45);
|
||||||
|
padding: 0.35rem 0.8rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.user-profile-btn-ghost:disabled{
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* Bio */
|
/* Bio */
|
||||||
|
|
||||||
.user-profile-bio {
|
.user-profile-bio {
|
||||||
|
|
@ -241,6 +259,12 @@ body[data-theme="light"] .user-profile-display-name {
|
||||||
color: rgba(30, 58, 138, 0.7);
|
color: rgba(30, 58, 138, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body[data-theme="light"] .user-profile-btn-ghost{
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
color: #1e293b;
|
||||||
|
border-color: rgba(99, 102, 241, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
body[data-theme="light"] .user-profile-bio {
|
body[data-theme="light"] .user-profile-bio {
|
||||||
background: rgba(255, 255, 255, 0.5);
|
background: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -496,6 +496,12 @@ body[data-theme="light"] .user-avatar{
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.sw-avatar i{ font-size: 12px; }
|
.sw-avatar i{ font-size: 12px; }
|
||||||
|
.sw-avatar img{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.sw-usertext{
|
.sw-usertext{
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue