Update frontend assets handling

This commit is contained in:
SocioWire 2026-01-08 15:40:58 +00:00
parent 713a35ab75
commit 642fc2d196
7 changed files with 400 additions and 4 deletions

View File

@ -30,6 +30,24 @@ export default function App() {
setWallError(next.error || "");
}, []);
const handlePostDeleted = useCallback((postId) => {
setWallPosts((prev) => prev.filter((p) => (p.id ?? p._id) !== postId));
if (selectedPost && (selectedPost.id ?? selectedPost._id) === postId) {
setSelectedPost(null);
}
}, [selectedPost]);
const handlePostUpdated = useCallback((updatedPost) => {
if (!updatedPost) return;
setWallPosts((prev) =>
prev.map((p) =>
(p.id ?? p._id) === (updatedPost.id ?? updatedPost._id)
? { ...p, ...updatedPost }
: p
)
);
}, []);
// charge le thème au démarrage
useEffect(() => {
if (typeof window === "undefined") return;
@ -186,6 +204,8 @@ export default function App() {
error={wallError}
selectedPost={selectedPost}
onSelectPost={setSelectedPost}
onPostDeleted={handlePostDeleted}
onPostUpdated={handlePostUpdated}
/>
</div>
{/* CHAT PANEL SUPPRIMÉ */}

View File

@ -225,3 +225,117 @@
background: rgba(248,113,113,0.25);
border-color: rgba(248,113,113,0.7);
}
/* Add Friend Form */
.friends-add-form {
display: flex;
gap: .25rem;
margin-bottom: .4rem;
}
.friends-add-input {
flex: 1;
padding: .35rem .5rem;
border-radius: 8px;
border: 1px solid rgba(71, 85, 105, 0.5);
background: rgba(15, 23, 42, 0.8);
color: #e5e7eb;
font-size: .75rem;
}
.friends-add-input::placeholder {
color: #64748b;
}
.friends-add-input:focus {
outline: none;
border-color: rgba(56, 189, 248, 0.6);
}
.friends-add-btn {
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid rgba(56, 189, 248, 0.4);
background: rgba(56, 189, 248, 0.15);
color: #7dd3fc;
font-size: .8rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.friends-add-btn:hover:not(:disabled) {
background: rgba(56, 189, 248, 0.25);
border-color: rgba(56, 189, 248, 0.7);
}
.friends-add-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.friends-add-error {
font-size: .7rem;
color: #fca5a5;
padding: .2rem .3rem;
margin-bottom: .3rem;
}
.friends-add-success {
font-size: .7rem;
color: #4ade80;
padding: .2rem .3rem;
margin-bottom: .3rem;
}
/* Suggestions dropdown */
.friends-add-wrap {
position: relative;
margin-bottom: .4rem;
}
.friends-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: rgba(15, 23, 42, 0.98);
border: 1px solid rgba(56, 189, 248, 0.4);
border-radius: 8px;
margin-top: 4px;
max-height: 180px;
overflow-y: auto;
z-index: 10;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.friends-suggestion-item {
display: flex;
align-items: center;
gap: .4rem;
padding: .4rem .5rem;
cursor: pointer;
font-size: .75rem;
color: #e5e7eb;
transition: background 0.15s;
}
.friends-suggestion-item:hover {
background: rgba(56, 189, 248, 0.15);
}
.friends-suggestion-item .friend-avatar {
width: 24px;
height: 24px;
font-size: .65rem;
}
.friends-suggestions-loading {
font-size: .7rem;
color: #64748b;
padding: .3rem .4rem;
text-align: center;
}

View File

@ -1,12 +1,14 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import {
fetchFriends,
fetchFriendRequests,
acceptFriendRequest,
rejectFriendRequest,
removeFriend,
sendFriendRequest,
} from '../../api/client';
import { useAuth } from '../../auth/AuthContext';
import { recoSearch } from '../../services/recoSearch';
import './Friends.css';
export default function FriendsList({ onSelectUser, onClose }) {
@ -16,6 +18,19 @@ export default function FriendsList({ onSelectUser, onClose }) {
const [requests, setRequests] = useState([]);
const [loading, setLoading] = useState(true);
// Add friend state
const [addUsername, setAddUsername] = useState('');
const [addLoading, setAddLoading] = useState(false);
const [addError, setAddError] = useState('');
const [addSuccess, setAddSuccess] = useState('');
// Autocomplete state
const [suggestions, setSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [suggestionsLoading, setSuggestionsLoading] = useState(false);
const searchTimeoutRef = useRef(null);
const inputRef = useRef(null);
useEffect(() => {
if (!authenticated) return;
@ -33,6 +48,58 @@ export default function FriendsList({ onSelectUser, onClose }) {
loadData();
}, [authenticated]);
// Search for users with debounce
const searchUsers = async (query) => {
if (!query || query.length < 2) {
setSuggestions([]);
setShowSuggestions(false);
return;
}
setSuggestionsLoading(true);
try {
const results = await recoSearch(query, { limit: 8 });
// Filter only profiles
const profiles = results.filter(
(r) => r.type === 'profile' || r.kind === 'profile'
);
setSuggestions(profiles);
setShowSuggestions(profiles.length > 0);
} catch {
setSuggestions([]);
}
setSuggestionsLoading(false);
};
const handleInputChange = (e) => {
const value = e.target.value;
setAddUsername(value);
// Debounce search
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
searchTimeoutRef.current = setTimeout(() => {
searchUsers(value);
}, 300);
};
const handleSelectSuggestion = (username) => {
setAddUsername(username);
setSuggestions([]);
setShowSuggestions(false);
inputRef.current?.focus();
};
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
};
}, []);
const handleAccept = async (username) => {
const res = await acceptFriendRequest(username);
if (res?.ok) {
@ -58,6 +125,28 @@ export default function FriendsList({ onSelectUser, onClose }) {
}
};
const handleAddFriend = async (e) => {
e.preventDefault();
const uname = addUsername.trim();
if (!uname) return;
setAddLoading(true);
setAddError('');
setAddSuccess('');
const res = await sendFriendRequest(uname);
setAddLoading(false);
if (res?.ok) {
setAddSuccess(`Friend request sent to ${uname}`);
setAddUsername('');
setTimeout(() => setAddSuccess(''), 3000);
} else {
setAddError(res?.error || res?.text || 'Could not send request');
setTimeout(() => setAddError(''), 4000);
}
};
if (!authenticated) {
return (
<div className="friends-panel">
@ -92,6 +181,53 @@ export default function FriendsList({ onSelectUser, onClose }) {
</button>
</div>
{/* Add Friend Form */}
<div className="friends-add-wrap">
<form className="friends-add-form" onSubmit={handleAddFriend}>
<input
ref={inputRef}
type="text"
className="friends-add-input"
placeholder="Add friend by username..."
value={addUsername}
onChange={handleInputChange}
onFocus={() => suggestions.length > 0 && setShowSuggestions(true)}
onBlur={() => setTimeout(() => setShowSuggestions(false), 200)}
disabled={addLoading}
autoComplete="off"
/>
<button
type="submit"
className="friends-add-btn"
disabled={addLoading || !addUsername.trim()}
title="Send friend request"
>
{addLoading ? '...' : <i className="fa-solid fa-user-plus" />}
</button>
</form>
{/* Suggestions dropdown */}
{showSuggestions && suggestions.length > 0 && (
<div className="friends-suggestions">
{suggestions.map((s) => (
<div
key={s.id}
className="friends-suggestion-item"
onMouseDown={() => handleSelectSuggestion(s.title || s.raw?.username || s.id)}
>
<div className="friend-avatar">
{(s.title || s.id)[0]?.toUpperCase() || '?'}
</div>
<span>{s.title || s.raw?.username || s.id}</span>
</div>
))}
</div>
)}
{suggestionsLoading && <div className="friends-suggestions-loading">Searching...</div>}
</div>
{addError && <div className="friends-add-error">{addError}</div>}
{addSuccess && <div className="friends-add-success">{addSuccess}</div>}
<div className="friends-content">
{loading ? (
<div className="friends-loading">Loading...</div>

View File

@ -1,9 +1,10 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useAuth } from "../../auth/AuthContext";
import { registerUser, updateProfileUsername } from "../../api/client";
import { registerUser, updateProfileUsername, fetchFriendRequests } from "../../api/client";
import AuthToast from "../Auth/AuthToast";
import { promptInstall, isRunningAsPWA, canInstall } from '../../pwaInstall';
import { trackEvent } from "../../utils/analytics";
import FriendsList from "../Friends/FriendsList";
const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" };
const LAST_SIGNUP_KEY = "sociowire:lastSignup";
@ -77,6 +78,8 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
const [showAuth, setShowAuth] = useState(false);
const [mode, setMode] = useState("login");
const [showUsernameModal, setShowUsernameModal] = useState(false);
const [showFriends, setShowFriends] = useState(false);
const [pendingRequestsCount, setPendingRequestsCount] = useState(0);
const [desiredUsername, setDesiredUsername] = useState("");
const [usernameError, setUsernameError] = useState("");
const [usernameInfo, setUsernameInfo] = useState("");
@ -162,6 +165,22 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
if (authenticated) setShowAuth(false);
}, [authenticated]);
// Fetch pending friend requests count
useEffect(() => {
if (!authenticated) {
setPendingRequestsCount(0);
return;
}
const loadRequests = async () => {
const res = await fetchFriendRequests();
setPendingRequestsCount(res?.requests?.length || 0);
};
loadRequests();
// Refresh every 30 seconds
const interval = setInterval(loadRequests, 30000);
return () => clearInterval(interval);
}, [authenticated]);
useEffect(() => {
if (!authenticated) {
setStableUsername("");
@ -477,6 +496,21 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
</button>
)}
{/* Friends button - only when authenticated */}
{authenticated && (
<button
className={"btn-action btn-action-friends" + (showFriends ? " is-active" : "")}
type="button"
onClick={() => setShowFriends(!showFriends)}
title="Friends"
>
<i className="fa-solid fa-user-group"></i>
{pendingRequestsCount > 0 && (
<span className="friends-badge-count">{pendingRequestsCount}</span>
)}
</button>
)}
{/* Login/Logout button */}
<button
className="btn-action btn-action-auth"
@ -664,6 +698,19 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
</div>
</div>
) : null}
{/* Friends Panel - dropdown from topbar */}
{showFriends && (
<div className="friends-dropdown-wrap">
<FriendsList
onSelectUser={(uname) => {
setShowFriends(false);
if (onUserIsland) onUserIsland(uname);
}}
onClose={() => setShowFriends(false)}
/>
</div>
)}
</>
);
}

View File

@ -1275,8 +1275,9 @@ export default function MapView({
const variants = data && data.variants ? data.variants : null;
const pickRef = (v) => {
if (!v) return "";
if (v.url) return v.url;
if (v.asset_id) return `asset://${v.asset_id}`;
return v.url || "";
return "";
};
const smallRef = variants ? pickRef(variants.small) : "";
const mediumRef = variants ? pickRef(variants.medium) : "";

View File

@ -5,7 +5,7 @@ function getKmFromPost(post) {
return post.km ?? post.distance_km ?? post.distanceKm ?? post.dist_km ?? null;
}
export default function PostList({ posts, loading, error, selectedPost, onSelectPost }) {
export default function PostList({ posts, loading, error, selectedPost, onSelectPost, onPostDeleted, onPostUpdated }) {
const [kmFilter, setKmFilter] = useState(1000);
const [visibleCount, setVisibleCount] = useState(6);
const listRef = useRef(null);
@ -66,6 +66,8 @@ export default function PostList({ posts, loading, error, selectedPost, onSelect
post={p}
selected={selectedPost && (selectedPost.id ?? selectedPost._id) === (p.id ?? p._id)}
onSelect={onSelectPost}
onPostDeleted={onPostDeleted}
onPostUpdated={onPostUpdated}
/>
))}

View File

@ -715,3 +715,79 @@ body[data-theme="light"] .sw-avatar{
.brand-under{ padding-left: 0; }
}
/* SW_TOPBAR_ONE_LINE_FIX:END */
/* SW_FRIENDS_BUTTON:BEGIN */
.btn-action-friends{
background: linear-gradient(135deg, #f59e0b, #d97706);
border-color: rgba(245, 158, 11, 0.5);
color: white;
}
.btn-action-friends:hover{
background: linear-gradient(135deg, #fbbf24, #f59e0b);
box-shadow: 0 4px 16px rgba(245, 158, 11, 0.6);
border-color: #fbbf24;
}
.btn-action-friends.is-active{
background: linear-gradient(135deg, #fbbf24, #f59e0b);
box-shadow: 0 4px 16px rgba(245, 158, 11, 0.6);
border-color: #fbbf24;
}
.btn-action-friends{
position: relative;
}
.friends-badge-count{
position: absolute;
top: -6px;
right: -6px;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 999px;
background: #ef4444;
color: white;
font-size: 0.65rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid rgba(15, 23, 42, 0.9);
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.5);
}
/* Friends dropdown panel positioning */
.friends-dropdown-wrap{
position: fixed;
top: 70px;
right: 16px;
z-index: 200;
animation: friendsSlideIn 0.2s ease-out;
}
@keyframes friendsSlideIn{
from{
opacity: 0;
transform: translateY(-10px);
}
to{
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 640px){
.friends-dropdown-wrap{
top: auto;
bottom: 80px;
right: 10px;
left: 10px;
}
.friends-dropdown-wrap .friends-panel{
max-width: none;
width: 100%;
}
}
/* SW_FRIENDS_BUTTON:END */