sw-fe/src/App.jsx

286 lines
9.5 KiB
JavaScript

import React, { Suspense, useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import TopBar from "./components/Layout/TopBar";
import PostList from "./components/Posts/PostList";
import ContentFilters from "./components/Filters/ContentFilters";
import IslandUniverse from "./components/Island/IslandUniverse";
import IslandViewer from "./components/Island/IslandViewer";
import ChatContainer from "./components/Chat/ChatContainer";
import { fetchIslandByUsername } from "./api/client";
const MapView = React.lazy(() => import("./components/Map/MapView"));
const THEME_KEY = "sociowire:theme";
const CONTENT_FILTER_KEY = "sociowire:contentFilter";
export default function App() {
const { t } = useTranslation();
const [theme, setTheme] = useState("blue");
// Clean up Facebook OAuth hash fragment (#_=_)
useEffect(() => {
if (window.location.hash === "#_=_") {
if (window.history.replaceState) {
window.history.replaceState(null, "", window.location.pathname + window.location.search);
} else {
window.location.hash = "";
}
}
}, []);
// Wall state (lifted from MapView)
const [wallPosts, setWallPosts] = useState([]);
const [wallLoading, setWallLoading] = useState(false);
const [wallError, setWallError] = useState("");
const [selectedPost, setSelectedPost] = useState(null);
// Content filter (shared between map and sociowall) - persisted
// "all" | "links" | "video" | "image" | "text" | "live" | "event"
const [contentFilter, setContentFilter] = useState(() => {
try {
const saved = localStorage.getItem(CONTENT_FILTER_KEY);
if (saved && ["all", "links", "video", "image", "text", "live", "event"].includes(saved)) {
return saved;
}
} catch {}
return "all";
});
// Save content filter to localStorage
useEffect(() => {
try {
localStorage.setItem(CONTENT_FILTER_KEY, contentFilter);
} catch {}
}, [contentFilter]);
// Island Universe & Island Viewer state
const [showUniverse, setShowUniverse] = useState(false);
const [selectedIsland, setSelectedIsland] = useState(null);
const handlePostsState = useCallback((next) => {
if (!next) return;
if (Array.isArray(next.posts)) setWallPosts(next.posts);
setWallLoading(!!next.loading);
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;
try {
const saved = localStorage.getItem(THEME_KEY);
if (saved === "dark" || saved === "blue" || saved === "light") {
setTheme(saved);
document.body.setAttribute("data-theme", saved);
} else {
setTheme("blue");
document.body.setAttribute("data-theme", "blue");
}
} catch (e) {
setTheme("blue");
document.body.setAttribute("data-theme", "blue");
}
}, []);
// applique / sauvegarde le thème
useEffect(() => {
if (typeof window === "undefined") return;
try {
document.body.setAttribute("data-theme", theme);
localStorage.setItem(THEME_KEY, theme);
} catch (e) {
// ignore
}
}, [theme]);
useEffect(() => {
const onScroll = () => {
const threshold = window.innerHeight * 0.55;
const expanded = window.scrollY > threshold;
document.body.classList.toggle("sw-wall-expanded", expanded);
};
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => window.removeEventListener("scroll", onScroll);
}, []);
// Island Universe handlers
const handleOpenUniverse = () => {
setShowUniverse(true);
};
const handleCloseUniverse = () => {
setShowUniverse(false);
};
const handleIslandClick = (island) => {
setShowUniverse(false); // Close universe
setSelectedIsland(island); // Open island viewer
};
const handleOpenUserIsland = async (uname) => {
const u = (uname || "").trim();
if (!u) return;
try {
const island = await fetchIslandByUsername(u);
if (island) {
setShowUniverse(false);
setSelectedIsland(island);
}
} catch {}
};
return (
<div className="app-root">
<TopBar
theme={theme}
onChangeTheme={setTheme}
onIslandsClick={handleOpenUniverse}
onUserIsland={handleOpenUserIsland}
/>
<div className="main-shell">
<div className="map-shell" id="map">
<Suspense
fallback={
<div className="map-page">
<div className="map-stage map-stage--placeholder">
<div className="map-placeholder-content">
<div className="map-placeholder-title">{t('map.loadingMap')}</div>
<div className="map-placeholder-subtitle">{t('map.warmingUp')}</div>
</div>
<div className="map-placeholder-overlay map-placeholder-top">
<div className="map-placeholder-pill map-placeholder-pill--wide" />
</div>
<div className="map-placeholder-overlay map-placeholder-left">
<div className="map-placeholder-pill" />
<div className="map-placeholder-pill" />
<div className="map-placeholder-pill" />
</div>
<div className="map-placeholder-overlay map-placeholder-right">
<div className="map-placeholder-pill" />
<div className="map-placeholder-pill" />
</div>
<div className="map-placeholder-overlay map-placeholder-bottom">
<div className="map-placeholder-chip" />
<div className="map-placeholder-chip" />
<div className="map-placeholder-chip" />
</div>
</div>
</div>
}
>
<MapView
theme={theme}
contentFilter={contentFilter}
onPostsState={handlePostsState}
selectedPost={selectedPost}
onSelectPost={setSelectedPost}
selectedIsland={selectedIsland}
onSelectIsland={setSelectedIsland}
onOpenIslands={handleOpenUniverse}
/>
</Suspense>
</div>
{/* BELOW THE MAP (page scroll like Facebook) */}
<div className="below-stage">
{/* Content filters - shared between map and sociowall */}
<ContentFilters
activeFilter={contentFilter}
onFilterChange={setContentFilter}
/>
<div className="below-row">
<div className="sociowall-panel" id="wall">
{/* SocioWall header inside panel */}
<div className="sociowall-header">
<h2 className="sociowall-title">SOCIOWALL</h2>
</div>
<PostList
posts={wallPosts}
loading={wallLoading}
error={wallError}
selectedPost={selectedPost}
onSelectPost={setSelectedPost}
onPostDeleted={handlePostDeleted}
onPostUpdated={handlePostUpdated}
contentFilter={contentFilter}
/>
</div>
</div>
</div>
<div style={{ height: 18 }} />
<section className="seo-section" id="islands">
<div className="seo-card">
<div className="seo-kicker">{t('islands.communities')}</div>
<h2>{t('islands.exploreIslands')}</h2>
<p>
{t('islands.islandsDescription')}
</p>
<button className="seo-cta" type="button" onClick={handleOpenUniverse}>
{t('islands.openIslands')}
</button>
</div>
</section>
{/* Chat System */}
<ChatContainer />
</div>
<footer className="sw-footer">
<div className="sw-footer-inner">
<div className="sw-footer-brand">SOCIOWIRE</div>
<div className="sw-footer-brand">© Sociowire v0.0016</div>
<div id="sw-weather-debug" style={{color: '#0ea5e9', fontSize: '10px'}}></div>
<nav className="sw-footer-nav" aria-label="Site">
<a href="/">{t('nav.home')}</a>
<a href="/#map">{t('nav.liveMap')}</a>
<a href="/#wall">{t('nav.sociowall')}</a>
<a href="/#islands">{t('nav.islands')}</a>
<a href="/about.html">{t('nav.about')}</a>
<a href="/contact.html">{t('nav.contact')}</a>
<a href="/privacy.html">{t('nav.privacy')}</a>
</nav>
</div>
</footer>
{/* Island Universe Modal */}
{showUniverse && (
<IslandUniverse
onClose={handleCloseUniverse}
onIslandClick={handleIslandClick}
/>
)}
{/* Island Viewer Modal */}
{selectedIsland && (
<IslandViewer
island={selectedIsland}
onClose={() => setSelectedIsland(null)}
/>
)}
</div>
);
}