goood
This commit is contained in:
parent
70251093a8
commit
4753cacdcc
51
src/App.jsx
51
src/App.jsx
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import React, { Suspense, useEffect, useState, useCallback } from "react";
|
||||
import TopBar from "./components/Layout/TopBar";
|
||||
import PostList from "./components/Posts/PostList";
|
||||
|
||||
// Lazy-load de la carte (gros chunk : maplibre, CSS, etc.)
|
||||
const MapView = React.lazy(() => import("./components/Map/MapView"));
|
||||
|
|
@ -9,6 +10,19 @@ const THEME_KEY = "sociowire:theme";
|
|||
export default function App() {
|
||||
const [theme, setTheme] = useState("blue");
|
||||
|
||||
// Wall state (lifted from MapView)
|
||||
const [wallPosts, setWallPosts] = useState([]);
|
||||
const [wallLoading, setWallLoading] = useState(false);
|
||||
const [wallError, setWallError] = useState("");
|
||||
const [selectedPost, setSelectedPost] = useState(null);
|
||||
|
||||
const handlePostsState = useCallback((next) => {
|
||||
if (!next) return;
|
||||
if (Array.isArray(next.posts)) setWallPosts(next.posts);
|
||||
setWallLoading(!!next.loading);
|
||||
setWallError(next.error || "");
|
||||
}, []);
|
||||
|
||||
// charge le thème au démarrage
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
|
@ -39,12 +53,45 @@ export default function App() {
|
|||
return (
|
||||
<div className="app-root">
|
||||
<TopBar theme={theme} onChangeTheme={setTheme} />
|
||||
|
||||
<div className="main-shell">
|
||||
<div className="map-shell">
|
||||
<Suspense fallback={<div style={{ padding: 16 }}>Loading map…</div>}>
|
||||
<MapView theme={theme} />
|
||||
<MapView
|
||||
theme={theme}
|
||||
onPostsState={handlePostsState}
|
||||
selectedPost={selectedPost}
|
||||
onSelectPost={setSelectedPost}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* BELOW THE MAP (page scroll like Facebook) */}
|
||||
<div className="below-stage">
|
||||
<div className="below-row">
|
||||
<div className="sociowall-panel">
|
||||
<div className="sociowall-header">SOCIOWALL</div>
|
||||
<PostList
|
||||
posts={wallPosts}
|
||||
loading={wallLoading}
|
||||
error={wallError}
|
||||
selectedPost={selectedPost}
|
||||
onSelectPost={setSelectedPost}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="chat-panel">
|
||||
<div className="chat-header">CHAT</div>
|
||||
<ul className="chat-users">
|
||||
<li>Yan</li>
|
||||
<li>Marc</li>
|
||||
<li>Fiso</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 18 }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,15 +11,24 @@ import { FILTER_MAIN_CATEGORIES, FILTER_CATEGORY_MAP, CREATE_CATEGORY_MAP } from
|
|||
import { useMapCore } from "./useMapCore";
|
||||
import { useUserPosition } from "./useUserPosition";
|
||||
import { usePostsEngine } from "./usePostsEngine";
|
||||
import PostList from "../Posts/PostList";
|
||||
import { useAuth } from "../../auth/AuthContext";
|
||||
import FilterButtons from "../Filters/FilterButtons";
|
||||
import TimeFilterButtons from "../Filters/TimeFilterButtons";
|
||||
|
||||
export default function MapView({ theme = "dark" }) {
|
||||
export default function MapView({
|
||||
theme = "dark",
|
||||
|
||||
// lift posts to App (so Sociowall is NOT inside the map)
|
||||
onPostsState,
|
||||
|
||||
// selection controlled by App
|
||||
selectedPost,
|
||||
onSelectPost,
|
||||
}) {
|
||||
const { authenticated, username, token } = useAuth();
|
||||
|
||||
const { containerRef, mapRef, markersRef, expandedElRef, viewParams, hasLastView } = useMapCore(theme);
|
||||
const { containerRef, mapRef, markersRef, expandedElRef, viewParams, hasLastView } =
|
||||
useMapCore(theme);
|
||||
|
||||
const [mainFilter, setMainFilter] = useState("All");
|
||||
const [timeFilter, setTimeFilter] = useState("RECENT");
|
||||
|
|
@ -37,8 +46,6 @@ export default function MapView({ theme = "dark" }) {
|
|||
expandedElRef,
|
||||
});
|
||||
|
||||
const [selectedPost, setSelectedPost] = useState(null);
|
||||
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [draftTitle, setDraftTitle] = useState("");
|
||||
const [draftBody, setDraftBody] = useState("");
|
||||
|
|
@ -50,11 +57,38 @@ export default function MapView({ theme = "dark" }) {
|
|||
|
||||
const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
|
||||
|
||||
// keep subFilter valid
|
||||
useEffect(() => {
|
||||
if (!bottomCategories.length) return;
|
||||
if (!subFilter || !bottomCategories.includes(subFilter)) setSubFilter("All");
|
||||
}, [mainFilter, bottomCategories, subFilter]);
|
||||
|
||||
// push posts state up to App (for Sociowall)
|
||||
useEffect(() => {
|
||||
if (!onPostsState) return;
|
||||
onPostsState({
|
||||
status,
|
||||
posts: visiblePosts,
|
||||
loading: loadingPosts,
|
||||
error: loadError,
|
||||
});
|
||||
}, [status, visiblePosts, loadingPosts, loadError, onPostsState]);
|
||||
|
||||
// if App selects a post (from Sociowall), fly to it on map
|
||||
useEffect(() => {
|
||||
if (!selectedPost) return;
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
const lng = selectedPost.lon ?? selectedPost.lng;
|
||||
const lat = selectedPost.lat ?? selectedPost.latitude;
|
||||
|
||||
if (typeof lng === "number" && typeof lat === "number") {
|
||||
map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 });
|
||||
}
|
||||
}, [selectedPost, mapRef]);
|
||||
|
||||
// Websocket for new posts
|
||||
useEffect(() => {
|
||||
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
const host =
|
||||
|
|
@ -108,6 +142,7 @@ export default function MapView({ theme = "dark" }) {
|
|||
const handleSubmitPost = async () => {
|
||||
if (isSaving) return;
|
||||
setSaveError("");
|
||||
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
|
|
@ -130,8 +165,8 @@ export default function MapView({ theme = "dark" }) {
|
|||
baseLat = center.lat;
|
||||
}
|
||||
|
||||
// keep marker visually above pointer
|
||||
const pixelOffsetY = -20;
|
||||
|
||||
const screenPoint = map.project([baseLng, baseLat]);
|
||||
const correctedPoint = { x: screenPoint.x, y: screenPoint.y + pixelOffsetY };
|
||||
const correctedLngLat = map.unproject([correctedPoint.x, correctedPoint.y]);
|
||||
|
|
@ -170,28 +205,36 @@ export default function MapView({ theme = "dark" }) {
|
|||
}
|
||||
};
|
||||
|
||||
const handleSelectPost = (post) => {
|
||||
setSelectedPost(post);
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
const lng = post.lon ?? post.lng;
|
||||
const lat = post.lat ?? post.latitude;
|
||||
if (typeof lng === "number" && typeof lat === "number") map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 });
|
||||
};
|
||||
|
||||
const handleMainFilterSelect = (code) => {
|
||||
if (!FILTER_MAIN_CATEGORIES.includes(code)) return;
|
||||
setMainFilter(code);
|
||||
};
|
||||
|
||||
// allow selecting from map later (optional hook)
|
||||
const handleSelectPost = (post) => {
|
||||
if (onSelectPost) onSelectPost(post);
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
const lng = post.lon ?? post.lng;
|
||||
const lat = post.lat ?? post.latitude;
|
||||
if (typeof lng === "number" && typeof lat === "number") {
|
||||
map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="map-page">\n <div className="map-stage">
|
||||
<div className="map-page">
|
||||
<div className="map-stage">
|
||||
<div ref={containerRef} className="map-container" />
|
||||
{status && <div className="map-status">{status}</div>}
|
||||
|
||||
<div className="map-overlay map-overlay-top">
|
||||
<button className="chip-pill" onClick={handleSearchClick}>Look at…</button>
|
||||
<button className="chip-pill" onClick={handleOpenCreate}>Place your wire</button>
|
||||
<button className="chip-pill" onClick={handleSearchClick}>
|
||||
Look at…
|
||||
</button>
|
||||
<button className="chip-pill" onClick={handleOpenCreate}>
|
||||
Place your wire
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="map-overlay map-overlay-left">
|
||||
|
|
@ -234,11 +277,15 @@ export default function MapView({ theme = "dark" }) {
|
|||
<div className="map-overlay create-post-panel">
|
||||
<div className="create-post-header">
|
||||
<span>Create wire</span>
|
||||
<button className="create-close" onClick={() => setIsCreating(false)}>✕</button>
|
||||
<button className="create-close" onClick={() => setIsCreating(false)}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span className="crosshair-label">
|
||||
Move the map, aim with the crosshair — your wire will be pinned there.
|
||||
</span>
|
||||
|
||||
<div className="create-row">
|
||||
<select
|
||||
value={draftCategory}
|
||||
|
|
@ -252,18 +299,21 @@ export default function MapView({ theme = "dark" }) {
|
|||
<option key={cat}>{cat}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select value={draftSubCategory} onChange={(e) => setDraftSubCategory(e.target.value)}>
|
||||
{(CREATE_CATEGORY_MAP[draftCategory] || CREATE_CATEGORY_MAP.News).map((sub) => (
|
||||
<option key={sub}>{sub}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<input
|
||||
className="create-input"
|
||||
placeholder="Short title..."
|
||||
value={draftTitle}
|
||||
onChange={(e) => setDraftTitle(e.target.value)}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
className="create-textarea"
|
||||
rows={2}
|
||||
|
|
@ -271,7 +321,9 @@ export default function MapView({ theme = "dark" }) {
|
|||
value={draftBody}
|
||||
onChange={(e) => setDraftBody(e.target.value)}
|
||||
/>
|
||||
|
||||
{saveError && <div className="create-error">{saveError}</div>}
|
||||
|
||||
<div className="create-actions">
|
||||
<button className="chip-pill" onClick={handleSubmitPost} disabled={isSaving}>
|
||||
{isSaving ? "Posting..." : "Post"}
|
||||
|
|
@ -279,37 +331,7 @@ export default function MapView({ theme = "dark" }) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* BELOW THE MAP (page scroll) */}
|
||||
<div className="below-stage">
|
||||
<div className="below-row">
|
||||
<div className="sociowall-panel">
|
||||
<div className="sociowall-header">Sociowall</div>
|
||||
<PostList
|
||||
posts={visiblePosts}
|
||||
loading={loadingPosts}
|
||||
error={loadError}
|
||||
selectedPost={selectedPost}
|
||||
onSelectPost={handleSelectPost}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="chat-panel">
|
||||
<div className="chat-header">Chat</div>
|
||||
<ul className="chat-users">
|
||||
<li>Yan</li>
|
||||
<li>Marc</li>
|
||||
<li>Fiso</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 24 }} />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,19 +17,42 @@ export const TEMPLATE_SPECS = {
|
|||
{ id: "meta", type: "text", x: 14, y: 88, w: 252, h: 16, bind: "data.source", style: "text.meta", maxLines: 1 }
|
||||
],
|
||||
},
|
||||
|
||||
/* Desktop/tablet full */
|
||||
full_spec: {
|
||||
size: { w: 360, h: 520 },
|
||||
size: { w: 360, h: 480 },
|
||||
background: { type: "solid", value: "surface" },
|
||||
radius: 22,
|
||||
layers: [
|
||||
{ id: "hero", type: "image", x: 0, y: 0, w: 360, h: 220, bind: "data.image" },
|
||||
{ id: "hero", type: "image", x: 0, y: 0, w: 360, h: 180, bind: "data.image" },
|
||||
{ id: "badge", type: "chip", x: 16, y: 16, text: "NEWS", style: "chip.news" },
|
||||
{ id: "title", type: "text", x: 16, y: 240, w: 328, h: 78, bind: "data.headline", style: "text.h1", maxLines: 3 },
|
||||
{ id: "summary", type: "text", x: 16, y: 325, w: 328, h: 110, bind: "data.summary", style: "text.body", maxLines: 5 },
|
||||
{ id: "title", type: "text", x: 16, y: 200, w: 328, h: 70, bind: "data.headline", style: "text.h1", maxLines: 3 },
|
||||
{ id: "summary", type: "text", x: 16, y: 272, w: 328, h: 110, bind: "data.summary", style: "text.body", maxLines: 4 },
|
||||
{
|
||||
id: "cta",
|
||||
type: "button",
|
||||
x: 16, y: 450, w: 328, h: 48,
|
||||
x: 16, y: 402, w: 328, h: 48,
|
||||
text: "Open source",
|
||||
style: "button.primary",
|
||||
action: { type: "open_url", bind: "data.url" }
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
/* ✅ Mobile full (shorter) */
|
||||
full_spec_mobile: {
|
||||
size: { w: 320, h: 380 },
|
||||
background: { type: "solid", value: "surface" },
|
||||
radius: 20,
|
||||
layers: [
|
||||
{ id: "hero", type: "image", x: 0, y: 0, w: 320, h: 120, bind: "data.image" },
|
||||
{ id: "badge", type: "chip", x: 14, y: 12, text: "NEWS", style: "chip.news" },
|
||||
{ id: "title", type: "text", x: 14, y: 140, w: 292, h: 58, bind: "data.headline", style: "text.h1", maxLines: 2 },
|
||||
{ id: "summary", type: "text", x: 14, y: 202, w: 292, h: 80, bind: "data.summary", style: "text.body", maxLines: 3 },
|
||||
{
|
||||
id: "cta",
|
||||
type: "button",
|
||||
x: 14, y: 300, w: 292, h: 46,
|
||||
text: "Open source",
|
||||
style: "button.primary",
|
||||
action: { type: "open_url", bind: "data.url" }
|
||||
|
|
@ -49,14 +72,23 @@ function normCat(post) {
|
|||
}
|
||||
|
||||
export function getTemplateKeyForPost(post) {
|
||||
// Future mapping: if (post.template_id === 101) return "news";
|
||||
return normCat(post);
|
||||
}
|
||||
|
||||
export function getTemplateSpecForPost(post, variant /* "mini"|"full" */) {
|
||||
const key = getTemplateKeyForPost(post);
|
||||
const t = TEMPLATE_SPECS[key] || TEMPLATE_SPECS.news;
|
||||
return variant === "full" ? t.full_spec : t.mini_spec;
|
||||
|
||||
if (variant === "full") {
|
||||
try {
|
||||
if (typeof window !== "undefined" && window.innerWidth <= 640 && t.full_spec_mobile) {
|
||||
return t.full_spec_mobile;
|
||||
}
|
||||
} catch {}
|
||||
return t.full_spec;
|
||||
}
|
||||
|
||||
return t.mini_spec;
|
||||
}
|
||||
|
||||
export function adaptPostToTemplateData(post) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
.app-root{ min-height:100vh; display:flex; flex-direction:column; }
|
||||
|
||||
/* IMPORTANT: allow scrolling under the map */
|
||||
/* allow scrolling under the map */
|
||||
.main-shell{ flex: 0 0 auto; display:block; padding:0; }
|
||||
.map-shell{ position:relative; width:100%; height:62vh; border-radius:0; overflow:hidden; border:none; background:#020617; }
|
||||
|
||||
/* MAP HEIGHT (make map taller so Sociowall/Chat sit lower) */
|
||||
.map-shell{
|
||||
position:relative;
|
||||
width:100%;
|
||||
height:68vh;
|
||||
border-radius:0;
|
||||
overflow:hidden;
|
||||
border:none;
|
||||
background:#020617;
|
||||
}
|
||||
|
||||
/* Phones: taller map => wall+chat lower */
|
||||
@media (max-width: 640px){
|
||||
.map-shell{ height:76vh; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
.map-page { display: flex; flex-direction: column; }
|
||||
.map-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Map stage (visible first). Then you scroll down for the wall. */
|
||||
/* Map stage should fill the map-shell height (no competing vh/clamp here) */
|
||||
.map-stage {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: clamp(520px, 72vh, 860px);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-container, .maplibre-container {
|
||||
.map-container,
|
||||
.maplibre-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
|
|
@ -26,17 +31,19 @@
|
|||
}
|
||||
|
||||
/* Below map area */
|
||||
.below-stage{
|
||||
padding: 0.25rem 0.7rem 1.0rem;
|
||||
.below-stage {
|
||||
padding: 0.25rem 0.7rem 1rem;
|
||||
}
|
||||
.below-row{
|
||||
display:flex;
|
||||
gap:.6rem;
|
||||
align-items:stretch;
|
||||
|
||||
.below-row {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* Mobile: stack */
|
||||
@media (max-width: 640px){
|
||||
.map-stage{ height: clamp(520px, 78vh, 900px); }
|
||||
.below-row{ flex-direction: column; }
|
||||
@media (max-width: 640px) {
|
||||
.below-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -633,3 +633,34 @@
|
|||
.post-pin--expanded .post-card-footer{ gap: 5px !important; }
|
||||
.post-pin--expanded .post-card-btn{ font-size: 10px !important; padding: 3px 7px !important; }
|
||||
}
|
||||
|
||||
/* === FINAL OVERRIDES (safe) === */
|
||||
|
||||
/* Keep pointers unchanged; only scale the mini template card */
|
||||
.sw-template-mini-wrap{
|
||||
transform: translate(-50%, calc(-100% - 12px)) scale(0.62) !important;
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px){
|
||||
.sw-template-mini-wrap{
|
||||
transform: translate(-50%, calc(-100% - 12px)) scale(0.52) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* === SW MOBILE: make expanded popup shorter (no pointer change) === */
|
||||
@media (max-width: 640px){
|
||||
.post-pin--expanded .post-card{
|
||||
max-height: 58vh !important;
|
||||
overflow: auto !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
/* keep right panel from making it tall */
|
||||
.sw-expanded-right{
|
||||
display:none !important;
|
||||
}
|
||||
.sw-news-generated{
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
align-items:stretch;
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.sociowall-panel {
|
||||
flex: 1.8;
|
||||
min-height: 160px;
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
margin-bottom:.25rem;
|
||||
}
|
||||
|
||||
.post-list { flex: 1; overflow-y: auto; padding-right: .15rem; max-height: 44vh; }
|
||||
.post-list { flex: 1; overflow-y: auto; padding-right: .15rem; max-height: 42vh; }
|
||||
.post-list-header { display:flex; align-items:center; gap:.5rem; margin-bottom:.25rem; font-size:.7rem; color:#cbd5f5; }
|
||||
.km-filter input[type="range"] { width:160px; }
|
||||
|
||||
|
|
@ -55,8 +56,6 @@
|
|||
border-color: #bfdbfe;
|
||||
box-shadow: 0 0 16px rgba(56,189,248,.6), 0 10px 22px rgba(15,23,42,.98);
|
||||
}
|
||||
.post-card h3 { margin:0 0 .12rem 0; font-size:.8rem; }
|
||||
.post-card p { margin:0; font-size:.72rem; opacity:.9; }
|
||||
|
||||
.chat-panel {
|
||||
width: 170px;
|
||||
|
|
@ -69,7 +68,6 @@
|
|||
display:flex;
|
||||
flex-direction:column;
|
||||
|
||||
/* stays visible beside wall while scrolling inside the list */
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
align-self: flex-start;
|
||||
|
|
@ -87,11 +85,18 @@
|
|||
.chat-users li { display:flex; align-items:center; gap:.3rem; padding:.18rem .1rem; }
|
||||
.chat-users li::before { content:"👤"; font-size:.72rem; opacity:.9; }
|
||||
|
||||
/* Mobile: stack */
|
||||
/* ✅ MOBILE: keep chat BESIDE sociowall (no stacking) */
|
||||
@media (max-width: 640px){
|
||||
.below-row{ flex-direction: column; }
|
||||
.chat-panel{ width: 100%; min-width: 0; position: static; }
|
||||
.post-list{ max-height: 46vh; }
|
||||
.below-row{ flex-direction: row; }
|
||||
.sociowall-panel{ flex: 2.2; }
|
||||
.chat-panel{
|
||||
width: 34vw;
|
||||
min-width: 140px;
|
||||
max-width: 180px;
|
||||
position: static;
|
||||
top: auto;
|
||||
}
|
||||
.post-list{ max-height: 36vh; }
|
||||
}
|
||||
|
||||
/* Guard: never render Sociowall overlay on top of the map */
|
||||
|
|
|
|||
Loading…
Reference in New Issue