feat: Add change password modal, small avatar images, fix email verification
- Add Change Password popup modal in Island profile - Convert avatar URLs to small variant (160x160) for thumbnails - Add toSmallImageUrl utility function in client.js - Update markerManager, IslandViewer, FriendsList, TopBar to use small images - Backend now stores small variant for avatar uploads 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
177f0034d6
commit
75c7b6e483
12
index.html
12
index.html
|
|
@ -9,9 +9,9 @@
|
|||
<link rel="sitemap" type="application/xml" title="Sitemap" href="/sitemap.xml" />
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="/logo2026.png" />
|
||||
<link rel="apple-touch-icon" href="/logo2026.png" />
|
||||
<link rel="shortcut icon" type="image/png" href="/logo2026.png" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="apple-touch-icon" href="/logo.png" />
|
||||
<link rel="shortcut icon" type="image/png" href="/logo.png" />
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
<!-- Meta pour partage (logo + titre + description) -->
|
||||
<meta property="og:title" content="SocioWire.com - Wired to life" />
|
||||
<meta property="og:description" content="Local news, events, market deals & friends on the map." />
|
||||
<meta property="og:image" content="https://sociowire.com/logo2026.png" />
|
||||
<meta property="og:image" content="https://sociowire.com/logo.png" />
|
||||
<meta property="og:url" content="https://sociowire.com" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="facebook-domain-verification" content="o4iph92mh9fgqx58sn1b77ia08h8cp" />
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="SocioWire.com - Wired to life" />
|
||||
<meta name="twitter:description" content="Local news, events, market deals & friends on the map." />
|
||||
<meta name="twitter:image" content="https://sociowire.com/logo2026.png" />
|
||||
<meta name="twitter:image" content="https://sociowire.com/logo.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
"@type": "Organization",
|
||||
"name": "SocioWire",
|
||||
"url": "https://sociowire.com",
|
||||
"logo": "https://sociowire.com/logo2026.png",
|
||||
"logo": "https://sociowire.com/logo.png",
|
||||
"sameAs": []
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
|
|
@ -4,6 +4,21 @@
|
|||
|
||||
import { loadAuth } from "../auth/authStorage";
|
||||
|
||||
/**
|
||||
* Convert image URL to small variant (160x160)
|
||||
* Asset URLs: ..._l.jpg -> ..._s.webp, ..._m.webp -> ..._s.webp
|
||||
*/
|
||||
export function toSmallImageUrl(url) {
|
||||
if (!url || typeof url !== "string") return url;
|
||||
if (url.includes("_l.jpg") || url.includes("_l.jpeg")) {
|
||||
return url.replace(/_l\.jpe?g/i, "_s.webp");
|
||||
}
|
||||
if (url.includes("_m.webp")) {
|
||||
return url.replace("_m.webp", "_s.webp");
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function apiBase() {
|
||||
const raw =
|
||||
(import.meta?.env?.VITE_API_BASE_URL || "")
|
||||
|
|
@ -1043,3 +1058,40 @@ export async function fetchWeatherPosts() {
|
|||
if (!res.ok) return [];
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change password for the authenticated user
|
||||
* @param {string} oldPassword - Current password (optional but recommended)
|
||||
* @param {string} newPassword - New password (min 8 chars)
|
||||
*/
|
||||
export async function changePassword(oldPassword, newPassword) {
|
||||
const url = `${apiBase()}/change-password`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json", ...authHeaders() },
|
||||
body: JSON.stringify({
|
||||
old_password: oldPassword || "",
|
||||
new_password: newPassword,
|
||||
}),
|
||||
});
|
||||
const text = await res.text().catch(() => "");
|
||||
let json = null;
|
||||
try {
|
||||
json = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
return {
|
||||
ok: res.ok,
|
||||
status: res.status,
|
||||
json,
|
||||
error: json?.error || (res.ok ? null : text || "Password change failed"),
|
||||
message: json?.message || "",
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn("changePassword error:", err);
|
||||
return { ok: false, status: 0, json: null, error: err?.message || "error" };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
removeFriend,
|
||||
sendFriendRequest,
|
||||
searchUsers,
|
||||
toSmallImageUrl,
|
||||
} from '../../api/client';
|
||||
import { useAuth } from '../../auth/AuthContext';
|
||||
import './Friends.css';
|
||||
|
|
@ -228,7 +229,7 @@ export default function FriendsList({ onSelectUser, onClose }) {
|
|||
>
|
||||
<div className="friend-avatar">
|
||||
{s.avatar_url ? (
|
||||
<img src={s.avatar_url} alt={s.username} />
|
||||
<img src={toSmallImageUrl(s.avatar_url)} alt={s.username} />
|
||||
) : (
|
||||
s.username[0]?.toUpperCase() || '?'
|
||||
)}
|
||||
|
|
@ -287,7 +288,7 @@ export default function FriendsList({ onSelectUser, onClose }) {
|
|||
>
|
||||
<div className="friend-avatar">
|
||||
{request.avatar_url ? (
|
||||
<img src={request.avatar_url} alt={request.username} />
|
||||
<img src={toSmallImageUrl(request.avatar_url)} alt={request.username} />
|
||||
) : (
|
||||
request.username[0]?.toUpperCase() || '?'
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||
import maplibregl from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import '../../styles/island.css';
|
||||
import { fetchPosts, updateProfileUsername, uploadProfileAvatar, updateTimezone, getBrowserTimezone } from '../../api/client';
|
||||
import { fetchPosts, updateProfileUsername, uploadProfileAvatar, updateTimezone, getBrowserTimezone, changePassword, toSmallImageUrl } from '../../api/client';
|
||||
import { setUserTimezone as saveTimezoneLocal, getUserTimezone as getStoredTimezone } from '../../utils/timezone';
|
||||
import { useAuth } from '../../auth/AuthContext';
|
||||
import { trackEvent } from "../../utils/analytics";
|
||||
|
|
@ -35,6 +35,14 @@ export default function IslandViewer({ island, onClose }) {
|
|||
const [timezone, setTimezone] = useState(getStoredTimezone());
|
||||
const [timezoneSaving, setTimezoneSaving] = useState(false);
|
||||
const [timezoneInfo, setTimezoneInfo] = useState("");
|
||||
// Password change
|
||||
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);
|
||||
const islandUsername = localUsername || island?.username || "";
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -206,7 +214,7 @@ export default function IslandViewer({ island, onClose }) {
|
|||
<div className="island-header-row">
|
||||
<div className="island-header-avatar">
|
||||
{avatarSrc ? (
|
||||
<img src={avatarSrc} alt={islandUsername} />
|
||||
<img src={toSmallImageUrl(avatarSrc)} alt={islandUsername} />
|
||||
) : (
|
||||
<span>{(islandUsername || "U")[0].toUpperCase()}</span>
|
||||
)}
|
||||
|
|
@ -222,7 +230,7 @@ export default function IslandViewer({ island, onClose }) {
|
|||
<div className="island-profile-panel">
|
||||
<div className="island-profile-avatar">
|
||||
{avatarSrc ? (
|
||||
<img src={avatarSrc} alt={islandUsername} />
|
||||
<img src={toSmallImageUrl(avatarSrc)} alt={islandUsername} />
|
||||
) : (
|
||||
<div className="island-profile-letter">
|
||||
{(islandUsername || "U")[0].toUpperCase()}
|
||||
|
|
@ -399,6 +407,23 @@ export default function IslandViewer({ island, onClose }) {
|
|||
</select>
|
||||
</div>
|
||||
{timezoneInfo ? <div className="island-profile-info">{timezoneInfo}</div> : null}
|
||||
|
||||
<div className="island-profile-row" style={{ marginTop: '16px' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="island-profile-btn"
|
||||
onClick={() => {
|
||||
setShowPasswordForm(true);
|
||||
setPasswordError('');
|
||||
setPasswordSuccess('');
|
||||
setOldPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-key" /> Change Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
@ -472,6 +497,85 @@ export default function IslandViewer({ island, onClose }) {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Change Modal */}
|
||||
{showPasswordForm && (
|
||||
<div className="password-modal-backdrop" onClick={() => setShowPasswordForm(false)}>
|
||||
<div className="password-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="password-modal-close" onClick={() => setShowPasswordForm(false)}>×</button>
|
||||
<h3 className="password-modal-title">Change Password</h3>
|
||||
<div className="password-modal-form">
|
||||
<div className="password-modal-field">
|
||||
<label>Current Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
</div>
|
||||
<div className="password-modal-field">
|
||||
<label>New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="Min 8 characters"
|
||||
/>
|
||||
</div>
|
||||
<div className="password-modal-field">
|
||||
<label>Confirm Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="Confirm new password"
|
||||
/>
|
||||
</div>
|
||||
{passwordError && <div className="password-modal-error">{passwordError}</div>}
|
||||
{passwordSuccess && <div className="password-modal-success">{passwordSuccess}</div>}
|
||||
<button
|
||||
type="button"
|
||||
className="password-modal-submit"
|
||||
disabled={passwordLoading}
|
||||
onClick={async () => {
|
||||
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!');
|
||||
setOldPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setTimeout(() => {
|
||||
setShowPasswordForm(false);
|
||||
setPasswordSuccess('');
|
||||
}, 1500);
|
||||
} else {
|
||||
if (res.error === 'invalid_old_password') {
|
||||
setPasswordError('Current password is incorrect');
|
||||
} else {
|
||||
setPasswordError(res.error || 'Failed to change password');
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{passwordLoading ? 'Changing...' : 'Change Password'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../../auth/AuthContext";
|
||||
import { registerUser, updateProfileUsername, fetchFriendRequests } from "../../api/client";
|
||||
import { registerUser, updateProfileUsername, fetchFriendRequests, toSmallImageUrl } from "../../api/client";
|
||||
import AuthToast from "../Auth/AuthToast";
|
||||
import { promptInstall, isRunningAsPWA, canInstall } from '../../pwaInstall';
|
||||
import { trackEvent } from "../../utils/analytics";
|
||||
|
|
@ -488,7 +488,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
|
|||
<header className="topbar">
|
||||
<div className="brand-stack">
|
||||
<div className="brand-toprow">
|
||||
<div className="logo-circle"><img src="/logo2026.png" alt="SocioWire" /></div>
|
||||
<div className="logo-circle"><img src="/logo.png" alt="SocioWire" /></div>
|
||||
<div className="title-block">
|
||||
<div className="main-title">SOCIOWIRE.com</div>
|
||||
<div className="sub-title">{t('topbar.wiredToLife')}</div>
|
||||
|
|
@ -524,7 +524,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
|
|||
onClick={() => onUserIsland && onUserIsland(username)}
|
||||
>
|
||||
<div className="sw-avatar">
|
||||
{avatarUrl ? <img src={avatarUrl} alt={username} /> : <span>{initialLetter(username)}</span>}
|
||||
{avatarUrl ? <img src={toSmallImageUrl(avatarUrl)} alt={username} /> : <span>{initialLetter(username)}</span>}
|
||||
</div>
|
||||
<div className="sw-usertext">
|
||||
<div className="sw-userline">{t('common.online')}</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? `<img src="${island.avatar_url}" style="width:100%;height:100%;object-fit:cover;" alt="${island.username}" />`
|
||||
? `<img src="${toSmallAvatarUrl(island.avatar_url)}" style="width:100%;height:100%;object-fit:cover;" alt="${island.username}" />`
|
||||
: `<div style="color:white;font-size:32px;font-weight:bold;">${island.username[0].toUpperCase()}</div>`
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,6 +296,7 @@ export default function UserProfile({ username, onClose, onVisitIsland }) {
|
|||
)}
|
||||
|
||||
{isSelf && (
|
||||
<>
|
||||
<button
|
||||
className="user-profile-btn user-profile-btn-ghost"
|
||||
onClick={() => {
|
||||
|
|
@ -259,9 +306,72 @@ export default function UserProfile({ username, onClose, onVisitIsland }) {
|
|||
>
|
||||
<i className="fa-solid fa-user-group" /> Friends
|
||||
</button>
|
||||
<button
|
||||
className="user-profile-btn user-profile-btn-ghost"
|
||||
onClick={() => {
|
||||
setShowPasswordForm(!showPasswordForm);
|
||||
setPasswordError('');
|
||||
setPasswordSuccess('');
|
||||
}}
|
||||
>
|
||||
<i className="fa-solid fa-key" /> Change Password
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isSelf && showPasswordForm && (
|
||||
<div className="user-profile-password-form">
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<div className="user-profile-form-group">
|
||||
<label>Current Password</label>
|
||||
<input
|
||||
type="password"
|
||||
className="user-profile-input"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
</div>
|
||||
<div className="user-profile-form-group">
|
||||
<label>New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
className="user-profile-input"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="Enter new password (min 8 chars)"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="user-profile-form-group">
|
||||
<label>Confirm New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
className="user-profile-input"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="Confirm new password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{passwordError && (
|
||||
<div className="user-profile-error">{passwordError}</div>
|
||||
)}
|
||||
{passwordSuccess && (
|
||||
<div className="user-profile-success">{passwordSuccess}</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
className="user-profile-btn user-profile-btn-primary"
|
||||
disabled={passwordLoading}
|
||||
>
|
||||
{passwordLoading ? 'Changing...' : 'Change Password'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSelf && showFriends && (
|
||||
<div className="user-profile-friends">
|
||||
{friends.length === 0 ? (
|
||||
|
|
@ -280,6 +390,7 @@ export default function UserProfile({ username, onClose, onVisitIsland }) {
|
|||
|
||||
<div className="user-profile-footer">
|
||||
<p className="user-profile-seed">Seed: {island.seed}</p>
|
||||
<p className="user-profile-version">v2026.01.10b</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue