This commit is contained in:
Your Name 2025-12-18 15:45:46 -05:00
parent 70251093a8
commit 4753cacdcc
7 changed files with 317 additions and 158 deletions

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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) {

View File

@ -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; }
}

View File

@ -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%;
@ -27,16 +32,18 @@
/* Below map area */
.below-stage {
padding: 0.25rem 0.7rem 1.0rem;
padding: 0.25rem 0.7rem 1rem;
}
.below-row {
display: flex;
gap:.6rem;
gap: 0.6rem;
align-items: stretch;
}
/* Mobile: stack */
@media (max-width: 640px) {
.map-stage{ height: clamp(520px, 78vh, 900px); }
.below-row{ flex-direction: column; }
.below-row {
flex-direction: column;
}
}

View File

@ -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;
}
}

View File

@ -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 */