Update frontend assets handling
This commit is contained in:
parent
713a35ab75
commit
642fc2d196
20
src/App.jsx
20
src/App.jsx
|
|
@ -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É */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) : "";
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue