SOCIOWIRE.com
{t('topbar.wiredToLife')}
@@ -524,7 +524,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
onClick={() => onUserIsland && onUserIsland(username)}
>
- {avatarUrl ?

:
{initialLetter(username)}}
+ {avatarUrl ?
})
:
{initialLetter(username)}}
{t('common.online')}
diff --git a/src/components/Map/markerManager.js b/src/components/Map/markerManager.js
index 5b00728..0333420 100644
--- a/src/components/Map/markerManager.js
+++ b/src/components/Map/markerManager.js
@@ -31,13 +31,30 @@ const avatarCache = new Map();
const avatarInflight = new Map();
const HEATMAP_ENABLED = false;
+/**
+ * Convert avatar URL to small variant (160x160)
+ * Asset URLs: ..._l.jpg -> ..._s.webp, ..._m.webp -> ..._s.webp
+ */
+function toSmallAvatarUrl(url) {
+ if (!url || typeof url !== "string") return url;
+ // Convert _l.jpg or _l.jpeg to _s.webp
+ if (url.includes("_l.jpg") || url.includes("_l.jpeg")) {
+ return url.replace(/_l\.jpe?g/i, "_s.webp");
+ }
+ // Convert _m.webp to _s.webp
+ if (url.includes("_m.webp")) {
+ return url.replace("_m.webp", "_s.webp");
+ }
+ return url;
+}
+
function applyAvatar(el, username, url) {
if (!el) return;
const initial = (username || "U")[0]?.toUpperCase() || "U";
el.innerHTML = "";
if (url) {
const img = document.createElement("img");
- img.src = url;
+ img.src = toSmallAvatarUrl(url);
img.alt = username || "user";
el.appendChild(img);
} else {
@@ -2589,7 +2606,7 @@ export function createIslandMarker(island, mapRef, markersRef, onIslandClick, th
transition: transform 0.2s ease, box-shadow 0.2s ease;
">
${island.avatar_url
- ? `

`
+ ? `
})
`
: `
${island.username[0].toUpperCase()}
`
}
diff --git a/src/components/Profile/UserProfile.jsx b/src/components/Profile/UserProfile.jsx
index c4d6039..2af3db4 100644
--- a/src/components/Profile/UserProfile.jsx
+++ b/src/components/Profile/UserProfile.jsx
@@ -6,6 +6,7 @@ import {
sendFriendRequest,
removeFriend,
fetchFriends,
+ changePassword,
} from '../../api/client';
import { useAuth } from '../../auth/AuthContext';
import '../../styles/profile.css';
@@ -38,6 +39,15 @@ export default function UserProfile({ username, onClose, onVisitIsland }) {
const [showFriends, setShowFriends] = useState(false);
const [friends, setFriends] = useState([]);
+ // Password change state
+ const [showPasswordForm, setShowPasswordForm] = useState(false);
+ const [oldPassword, setOldPassword] = useState('');
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [passwordError, setPasswordError] = useState('');
+ const [passwordSuccess, setPasswordSuccess] = useState('');
+ const [passwordLoading, setPasswordLoading] = useState(false);
+
useEffect(() => {
if (!authenticated || isSelf || !username) return;
@@ -100,6 +110,42 @@ export default function UserProfile({ username, onClose, onVisitIsland }) {
setFriends(res?.friends || []);
};
+ const handleChangePassword = async (e) => {
+ e.preventDefault();
+ setPasswordError('');
+ setPasswordSuccess('');
+
+ if (newPassword.length < 8) {
+ setPasswordError('Password must be at least 8 characters');
+ return;
+ }
+ if (newPassword !== confirmPassword) {
+ setPasswordError('Passwords do not match');
+ return;
+ }
+
+ setPasswordLoading(true);
+ const res = await changePassword(oldPassword, newPassword);
+ setPasswordLoading(false);
+
+ if (res.ok) {
+ setPasswordSuccess('Password changed successfully!');
+ setOldPassword('');
+ setNewPassword('');
+ setConfirmPassword('');
+ setTimeout(() => {
+ setShowPasswordForm(false);
+ setPasswordSuccess('');
+ }, 2000);
+ } else {
+ if (res.error === 'invalid_old_password') {
+ setPasswordError('Current password is incorrect');
+ } else {
+ setPasswordError(res.error || 'Failed to change password');
+ }
+ }
+ };
+
const handleAvatarChange = async (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;
@@ -250,18 +296,82 @@ export default function UserProfile({ username, onClose, onVisitIsland }) {
)}
{isSelf && (
-
+ <>
+
+
+ >
)}
{friends.length === 0 ? (
@@ -280,6 +390,7 @@ export default function UserProfile({ username, onClose, onVisitIsland }) {
Seed: {island.seed}
+
v2026.01.10b
>
) : (
diff --git a/src/styles/island.css b/src/styles/island.css
index 1fbfa06..3ab5972 100644
--- a/src/styles/island.css
+++ b/src/styles/island.css
@@ -510,3 +510,181 @@ body[data-theme="light"] .island-viewer-close {
body[data-theme="light"] .island-viewer-close:hover {
background: rgba(30, 58, 138, 0.3);
}
+
+/* Password Form */
+.island-password-form {
+ margin-top: 0.75rem;
+ padding: 0.75rem;
+ border-radius: 12px;
+ background: rgba(15, 23, 42, 0.5);
+ border: 1px solid rgba(148, 163, 184, 0.2);
+}
+
+.island-password-form .island-profile-row {
+ margin-bottom: 0.5rem;
+}
+
+.island-password-form .island-profile-row label {
+ min-width: 100px;
+}
+
+.island-profile-btn-primary {
+ background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
+ border-color: rgba(99, 102, 241, 0.6);
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
+}
+
+.island-profile-btn-primary:hover:not(:disabled) {
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.5);
+}
+
+body[data-theme="light"] .island-password-form {
+ background: rgba(255, 255, 255, 0.5);
+}
+
+body[data-theme="light"] .island-profile-btn-primary {
+ background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
+}
+
+/* Password Change Modal */
+.password-modal-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 10000;
+ background: rgba(0, 0, 0, 0.75);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(4px);
+ animation: fadeIn 0.2s ease;
+}
+
+.password-modal {
+ position: relative;
+ width: 90%;
+ max-width: 380px;
+ background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
+ border-radius: 16px;
+ padding: 1.5rem;
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
+ border: 1px solid rgba(56, 189, 248, 0.2);
+ animation: slideUp 0.25s ease;
+}
+
+.password-modal-close {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.1);
+ border: none;
+ color: white;
+ font-size: 18px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.password-modal-close:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.password-modal-title {
+ color: #f1f5f9;
+ font-size: 1.25rem;
+ margin: 0 0 1.25rem 0;
+ font-weight: 700;
+}
+
+.password-modal-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.password-modal-field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+.password-modal-field label {
+ color: rgba(226, 232, 240, 0.8);
+ font-size: 0.8rem;
+ font-weight: 600;
+}
+
+.password-modal-field input {
+ background: rgba(15, 23, 42, 0.6);
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ color: #e2e8f0;
+ border-radius: 10px;
+ padding: 0.6rem 0.75rem;
+ font-size: 0.9rem;
+}
+
+.password-modal-field input:focus {
+ outline: none;
+ border-color: rgba(56, 189, 248, 0.5);
+}
+
+.password-modal-error {
+ color: #f87171;
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.2);
+ padding: 0.5rem 0.75rem;
+ border-radius: 8px;
+ font-size: 0.8rem;
+}
+
+.password-modal-success {
+ color: #86efac;
+ background: rgba(34, 197, 94, 0.1);
+ border: 1px solid rgba(34, 197, 94, 0.2);
+ padding: 0.5rem 0.75rem;
+ border-radius: 8px;
+ font-size: 0.8rem;
+}
+
+.password-modal-submit {
+ background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
+ border: none;
+ color: white;
+ padding: 0.75rem 1rem;
+ border-radius: 10px;
+ font-size: 0.95rem;
+ font-weight: 700;
+ cursor: pointer;
+ margin-top: 0.5rem;
+}
+
+.password-modal-submit:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.password-modal-submit:hover:not(:disabled) {
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
+}
+
+body[data-theme="light"] .password-modal {
+ background: linear-gradient(135deg, #e0e7ff 0%, #f5f3ff 100%);
+ border-color: rgba(99, 102, 241, 0.3);
+}
+
+body[data-theme="light"] .password-modal-title {
+ color: #1e3a8a;
+}
+
+body[data-theme="light"] .password-modal-field label {
+ color: rgba(30, 58, 138, 0.8);
+}
+
+body[data-theme="light"] .password-modal-field input {
+ background: rgba(255, 255, 255, 0.8);
+ color: #1e293b;
+ border-color: rgba(99, 102, 241, 0.3);
+}
diff --git a/src/styles/profile.css b/src/styles/profile.css
index bef103e..f97814e 100644
--- a/src/styles/profile.css
+++ b/src/styles/profile.css
@@ -240,6 +240,12 @@ body[data-theme="blue"] .user-profile-container{
margin: 0;
}
+.user-profile-version {
+ color: rgba(255, 255, 255, 0.3);
+ font-size: 0.7rem;
+ margin: 4px 0 0 0;
+}
+
/* Friends list in profile */
.user-profile-friends {
margin-top: 0.75rem;
@@ -382,3 +388,76 @@ body[data-theme="light"] .user-profile-close:hover {
body[data-theme="light"] .user-profile-footer {
border-top-color: rgba(30, 58, 138, 0.1);
}
+
+/* Password Change Form */
+.user-profile-password-form {
+ margin-top: 0.75rem;
+ padding: 1rem;
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 12px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.user-profile-form-group {
+ margin-bottom: 0.75rem;
+}
+
+.user-profile-form-group label {
+ display: block;
+ color: rgba(255, 255, 255, 0.8);
+ font-size: 0.8rem;
+ font-weight: 600;
+ margin-bottom: 0.35rem;
+}
+
+.user-profile-form-group .user-profile-input {
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.85rem;
+}
+
+.user-profile-error {
+ color: #fca5a5;
+ background: rgba(239, 68, 68, 0.15);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+ padding: 0.5rem 0.75rem;
+ border-radius: 8px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ margin-bottom: 0.75rem;
+}
+
+.user-profile-success {
+ color: #86efac;
+ background: rgba(34, 197, 94, 0.15);
+ border: 1px solid rgba(34, 197, 94, 0.3);
+ padding: 0.5rem 0.75rem;
+ border-radius: 8px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ margin-bottom: 0.75rem;
+}
+
+.user-profile-password-form .user-profile-btn-primary {
+ width: 100%;
+ justify-content: center;
+ margin-top: 0.5rem;
+}
+
+body[data-theme="light"] .user-profile-password-form {
+ background: rgba(255, 255, 255, 0.5);
+}
+
+body[data-theme="light"] .user-profile-form-group label {
+ color: rgba(30, 58, 138, 0.8);
+}
+
+body[data-theme="light"] .user-profile-error {
+ color: #dc2626;
+ background: rgba(239, 68, 68, 0.1);
+}
+
+body[data-theme="light"] .user-profile-success {
+ color: #16a34a;
+ background: rgba(34, 197, 94, 0.1);
+}