Auto deploy: 2025-12-09 01:46:49
This commit is contained in:
parent
222a907dd2
commit
1acb77f7d3
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
|
||||
FILE="src/components/Map/MapView.jsx"
|
||||
|
||||
echo "🛠 Fix MapLibre CSS import in $FILE"
|
||||
|
||||
# Remplace l'ancien mauvais import par le bon
|
||||
sed -i 's|import "maplibregl/dist/maplibre-gl.css";|import "maplibre-gl/dist/maplibre-gl.css";|g' "$FILE"
|
||||
|
||||
# Vérifie le résultat
|
||||
echo "🔍 Vérification de l'import..."
|
||||
grep 'maplibre-gl' "$FILE" || echo "⚠️ Aucun import trouvé !"
|
||||
|
||||
echo "✅ Correction terminée ! Maintenant lance : npm run dev"
|
||||
15
src/App.jsx
15
src/App.jsx
|
|
@ -1,23 +1,16 @@
|
|||
import React from "react";
|
||||
import TopBar from "./components/Layout/TopBar";
|
||||
import PageContainer from "./components/Layout/PageContainer";
|
||||
import MapView from "./components/Map/MapView";
|
||||
import PostList from "./components/Posts/PostList";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="app-root">
|
||||
<TopBar />
|
||||
<PageContainer>
|
||||
<div className="layout-main">
|
||||
<div className="layout-left">
|
||||
<PostList />
|
||||
</div>
|
||||
<div className="layout-right">
|
||||
<MapView />
|
||||
</div>
|
||||
<div className="main-shell">
|
||||
<div className="map-shell">
|
||||
<MapView />
|
||||
</div>
|
||||
</PageContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,59 @@
|
|||
import axios from "axios";
|
||||
/**
|
||||
* SocioWire API client
|
||||
* Frontend -> Backend (port 8081 via proxy /api)
|
||||
*/
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: "http://localhost:8080", // ton backend SocioWire
|
||||
timeout: 10000,
|
||||
});
|
||||
const API_BASE = "/api";
|
||||
|
||||
/** Récupère la liste des posts */
|
||||
export async function fetchPosts() {
|
||||
const res = await api.get("/posts");
|
||||
return res.data;
|
||||
const res = await fetch(`${API_BASE}/posts`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error("fetchPosts failed", res.status);
|
||||
throw new Error("Failed to load posts");
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return Array.isArray(data) ? data : data.items || [];
|
||||
}
|
||||
|
||||
export default api;
|
||||
/**
|
||||
* Récupère la position approx de l'utilisateur.
|
||||
* Le backend doit renvoyer { lat, lon } ou { latitude, longitude } etc.
|
||||
*/
|
||||
export async function fetchUserLocation() {
|
||||
const res = await fetch(`${API_BASE}/location`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.warn("fetchUserLocation failed", res.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const lat =
|
||||
data.lat ??
|
||||
data.latitude ??
|
||||
data.location?.lat ??
|
||||
data.location?.latitude;
|
||||
|
||||
const lon =
|
||||
data.lon ??
|
||||
data.lng ??
|
||||
data.longitude ??
|
||||
data.location?.lon ??
|
||||
data.location?.lng ??
|
||||
data.location?.longitude;
|
||||
|
||||
if (typeof lat === "number" && typeof lon === "number") {
|
||||
return [lon, lat]; // [lng, lat] pour MapLibre
|
||||
}
|
||||
|
||||
console.warn("fetchUserLocation: invalid payload", data);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import React from "react";
|
|||
export default function TopBar() {
|
||||
return (
|
||||
<header className="topbar">
|
||||
<div className="topbar-title">SocioWire</div>
|
||||
<div className="topbar-actions">
|
||||
<button className="btn-primary">Reload posts</button>
|
||||
<div className="logo-circle">SW</div>
|
||||
<div className="title-block">
|
||||
<div className="main-title">SOCIOWIRE.com</div>
|
||||
<div className="sub-title">Wired to life</div>
|
||||
</div>
|
||||
<button className="btn-primary">Login</button>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,297 @@
|
|||
import React, { useEffect, useRef } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import { fetchPosts } from "../../api/client";
|
||||
|
||||
/* ------------ UTILS -------------- */
|
||||
function getCoords(post) {
|
||||
const lat =
|
||||
post.lat ??
|
||||
post.latitude ??
|
||||
post.location?.lat ??
|
||||
(Array.isArray(post.coords) ? post.coords[1] : null);
|
||||
|
||||
const lon =
|
||||
post.lon ??
|
||||
post.lng ??
|
||||
post.location?.lng ??
|
||||
(Array.isArray(post.coords) ? post.coords[0] : null);
|
||||
|
||||
if (typeof lat === "number" && typeof lon === "number") return [lon, lat];
|
||||
return null;
|
||||
}
|
||||
|
||||
function extract(post) {
|
||||
const title = post.title || post.text || post.body || "Post";
|
||||
const body =
|
||||
post.body || post.content || post.text || "No additional details.";
|
||||
const short = title.length > 28 ? title.slice(0, 25) + "..." : title;
|
||||
return { title, body, short };
|
||||
}
|
||||
|
||||
/* ------------ BOTTOM CATEGORIES -------------- */
|
||||
const CATEGORY_MAP = {
|
||||
News: ["World", "Politics", "Tech", "Finance", "Local"],
|
||||
Friends: ["Nearby", "Chats", "Groups", "Stories", "Favs"],
|
||||
Events: ["Today", "Weekend", "Music", "Sports", "Local"],
|
||||
Market: ["Actu", "Tech", "Finan", "Sport", "Deals"],
|
||||
Default: ["World", "Actu", "Tech", "Finan", "Sport"],
|
||||
};
|
||||
|
||||
export default function MapView() {
|
||||
const mapContainerRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const mapRef = useRef(null);
|
||||
const markersRef = useRef([]); // { marker, el, compactHTML, expandedHTML }
|
||||
const expandedElRef = useRef(null);
|
||||
|
||||
const [status, setStatus] = useState("Loading posts...");
|
||||
const [mapReady, setMapReady] = useState(false);
|
||||
const [mainFilter, setMainFilter] = useState("Default");
|
||||
|
||||
/* ------------ INIT MAP -------------- */
|
||||
useEffect(() => {
|
||||
if (mapRef.current) return;
|
||||
if (!containerRef.current) return;
|
||||
|
||||
mapRef.current = new maplibregl.Map({
|
||||
container: mapContainerRef.current,
|
||||
const map = new maplibregl.Map({
|
||||
container: containerRef.current,
|
||||
style: "https://demotiles.maplibre.org/style.json",
|
||||
center: [-73.5673, 45.5017], // Montréal par défaut
|
||||
zoom: 11,
|
||||
center: [-70, 47],
|
||||
zoom: 5,
|
||||
pitch: 45,
|
||||
bearing: -20,
|
||||
antialias: true,
|
||||
});
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||
|
||||
mapRef.current = map;
|
||||
setMapReady(true);
|
||||
|
||||
return () => {
|
||||
if (mapRef.current) {
|
||||
mapRef.current.remove();
|
||||
mapRef.current = null;
|
||||
}
|
||||
markersRef.current.forEach((m) => m.marker.remove());
|
||||
markersRef.current = [];
|
||||
map.remove();
|
||||
mapRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
/* ------------ CENTRER SUR POSITION USER -------------- */
|
||||
useEffect(() => {
|
||||
if (!mapReady || !mapRef.current) return;
|
||||
|
||||
const map = mapRef.current;
|
||||
let cancelled = false;
|
||||
|
||||
function flyTo(lat, lon) {
|
||||
if (cancelled) return;
|
||||
map.flyTo({
|
||||
center: [lon, lat],
|
||||
zoom: 10,
|
||||
speed: 1.4,
|
||||
curve: 1.6,
|
||||
});
|
||||
}
|
||||
|
||||
async function fallbackIp() {
|
||||
try {
|
||||
const res = await fetch("/api/location"); // <-- adapte si ton endpoint est différent
|
||||
if (!res.ok) return;
|
||||
const data = await res.json(); // attendu: { lat: number, lon: number }
|
||||
if (
|
||||
typeof data.lat === "number" &&
|
||||
typeof data.lon === "number" &&
|
||||
!Number.isNaN(data.lat) &&
|
||||
!Number.isNaN(data.lon)
|
||||
) {
|
||||
flyTo(data.lat, data.lon);
|
||||
}
|
||||
} catch (e) {
|
||||
// pas grave, on garde le centre par défaut
|
||||
console.warn("IP geo fallback failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if ("geolocation" in navigator) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
if (cancelled) return;
|
||||
flyTo(pos.coords.latitude, pos.coords.longitude);
|
||||
},
|
||||
() => {
|
||||
if (cancelled) return;
|
||||
fallbackIp();
|
||||
},
|
||||
{ enableHighAccuracy: true, timeout: 8000 }
|
||||
);
|
||||
} else {
|
||||
fallbackIp();
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [mapReady]);
|
||||
|
||||
/* ------------ LOAD POSTS -------------- */
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
setStatus("Loading posts...");
|
||||
const raw = await fetchPosts();
|
||||
const posts = Array.isArray(raw) ? raw : raw?.items || [];
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
markersRef.current.forEach((m) => m.marker.remove());
|
||||
markersRef.current = [];
|
||||
expandedElRef.current = null;
|
||||
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
let hasCoords = false;
|
||||
|
||||
posts.forEach((post) => {
|
||||
const coords = getCoords(post);
|
||||
if (!coords) return;
|
||||
|
||||
const { title, body, short } = extract(post);
|
||||
|
||||
const compactHTML = `
|
||||
<div class="marker-inner">
|
||||
<div class="marker-dot"></div>
|
||||
<span class="marker-text">${short}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const expandedHTML = `
|
||||
<div class="marker-expanded-inner">
|
||||
<div class="marker-expanded-header">
|
||||
<div class="marker-dot"></div>
|
||||
<span class="marker-expanded-title">${title}</span>
|
||||
</div>
|
||||
<div class="marker-expanded-body">${body}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const el = document.createElement("div");
|
||||
el.className = "post-marker post-marker-compact";
|
||||
el.innerHTML = compactHTML;
|
||||
|
||||
const marker = new maplibregl.Marker({ element: el, anchor: "bottom" })
|
||||
.setLngLat(coords)
|
||||
.addTo(map);
|
||||
|
||||
const meta = { marker, el, compactHTML, expandedHTML };
|
||||
markersRef.current.push(meta);
|
||||
|
||||
const toggle = () => {
|
||||
if (expandedElRef.current && expandedElRef.current !== el) {
|
||||
const prev = markersRef.current.find(
|
||||
(m) => m.el === expandedElRef.current
|
||||
);
|
||||
if (prev) {
|
||||
prev.el.className = "post-marker post-marker-compact";
|
||||
prev.el.innerHTML = prev.compactHTML;
|
||||
}
|
||||
}
|
||||
|
||||
if (expandedElRef.current === el) {
|
||||
el.className = "post-marker post-marker-compact";
|
||||
el.innerHTML = compactHTML;
|
||||
expandedElRef.current = null;
|
||||
} else {
|
||||
el.className = "post-marker post-marker-expanded";
|
||||
el.innerHTML = expandedHTML;
|
||||
expandedElRef.current = el;
|
||||
}
|
||||
};
|
||||
|
||||
el.addEventListener("click", toggle);
|
||||
el.addEventListener("touchend", (e) => {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
});
|
||||
|
||||
bounds.extend(coords);
|
||||
hasCoords = true;
|
||||
});
|
||||
|
||||
if (hasCoords) {
|
||||
map.fitBounds(bounds, { padding: 40, maxZoom: 13 });
|
||||
}
|
||||
|
||||
setStatus(posts.length ? "" : "No posts found.");
|
||||
} catch (err) {
|
||||
console.error("Erreur chargement posts:", err);
|
||||
if (!cancelled) setStatus("Error loading posts.");
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const bottomCategories = CATEGORY_MAP[mainFilter] || CATEGORY_MAP.Default;
|
||||
|
||||
return (
|
||||
<div className="map-view">
|
||||
<div ref={mapContainerRef} className="maplibre-container" />
|
||||
<div ref={containerRef} className="map-container" />
|
||||
{status && <div className="map-status">{status}</div>}
|
||||
|
||||
{/* TOP */}
|
||||
<div className="map-overlay map-overlay-top">
|
||||
<div className="lookat-box">Look at…</div>
|
||||
<div className="wire-title">Place your wire</div>
|
||||
<div className="zoom-buttons">
|
||||
<button className="chip-hex">Zoom In</button>
|
||||
<button className="chip-hex">Zoom Out</button>
|
||||
<button className="chip-hex">Swall</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LEFT */}
|
||||
<div className="map-overlay map-overlay-left">
|
||||
<button className="chip-round">Now</button>
|
||||
<button className="chip-round">Today</button>
|
||||
<button className="chip-round">Recent</button>
|
||||
<button className="chip-round">Past</button>
|
||||
</div>
|
||||
|
||||
{/* RIGHT — filtres principaux */}
|
||||
<div className="map-overlay map-overlay-right">
|
||||
{["News", "Friends", "Events", "Market"].map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
className={
|
||||
mainFilter === cat ? "chip-pill chip-selected" : "chip-pill"
|
||||
}
|
||||
onClick={() => setMainFilter(cat)}
|
||||
>
|
||||
{cat}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* BOTTOM — arc/orbite sous le globe */}
|
||||
<div className="map-overlay map-overlay-bottom">
|
||||
{bottomCategories.map((c, idx) => (
|
||||
<button
|
||||
key={c}
|
||||
className={
|
||||
idx === 0 ? "chip-egg chip-egg-active" : "chip-egg"
|
||||
}
|
||||
>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,17 +10,27 @@ function getKmFromPost(post) {
|
|||
);
|
||||
}
|
||||
|
||||
export default function PostCard({ post }) {
|
||||
const km = getKmFromPost(post);
|
||||
export default function PostCard({ post, selected, onSelect }) {
|
||||
const title = post.title || post.name || "Post";
|
||||
|
||||
const fullBody =
|
||||
post.body || post.content || post.text || "";
|
||||
|
||||
// texte court dans le Sociowall
|
||||
const body =
|
||||
post.body ||
|
||||
post.content ||
|
||||
post.text ||
|
||||
JSON.stringify(post, null, 2);
|
||||
fullBody.length > 80 ? fullBody.slice(0, 77) + "..." : fullBody;
|
||||
|
||||
const km = getKmFromPost(post);
|
||||
|
||||
function handleClick() {
|
||||
if (onSelect) onSelect(post);
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="post-card">
|
||||
<article
|
||||
className={"post-card" + (selected ? " selected" : "")}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<h3>{title}</h3>
|
||||
<p>{body}</p>
|
||||
{km != null && <span className="post-km">{km} km</span>}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import PostCard from "./PostCard";
|
||||
import { fetchPosts } from "../../api/client";
|
||||
|
||||
const FALLBACK_POSTS = [
|
||||
{ id: 1, title: "Exemple local", body: "Backend non joignable, posts mock.", km: 2 },
|
||||
{ id: 2, title: "Deuxième post", body: "On repart en neuf propre.", km: 10 },
|
||||
];
|
||||
|
||||
function getKmFromPost(post) {
|
||||
return (
|
||||
|
|
@ -17,57 +11,28 @@ function getKmFromPost(post) {
|
|||
);
|
||||
}
|
||||
|
||||
export default function PostList() {
|
||||
const [posts, setPosts] = useState([]);
|
||||
export default function PostList({
|
||||
posts,
|
||||
loading,
|
||||
error,
|
||||
selectedPost,
|
||||
onSelectPost,
|
||||
}) {
|
||||
const [kmFilter, setKmFilter] = useState(1000);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
const data = await fetchPosts();
|
||||
if (!cancelled) {
|
||||
if (Array.isArray(data)) {
|
||||
setPosts(data);
|
||||
} else if (Array.isArray(data?.items)) {
|
||||
setPosts(data.items);
|
||||
} else {
|
||||
setPosts(FALLBACK_POSTS);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Erreur fetchPosts:", err);
|
||||
if (!cancelled) {
|
||||
setError("Impossible de charger les posts (backend).");
|
||||
setPosts(FALLBACK_POSTS);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filtered = posts.filter((p) => {
|
||||
const km = getKmFromPost(p);
|
||||
if (km == null) return true; // si pas de distance, on garde
|
||||
return km <= kmFilter;
|
||||
});
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
posts.filter((p) => {
|
||||
const km = getKmFromPost(p);
|
||||
if (km == null) return true;
|
||||
return km <= kmFilter;
|
||||
}),
|
||||
[posts, kmFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="post-list">
|
||||
<div className="post-list-header">
|
||||
<h2>Posts</h2>
|
||||
<div className="km-filter">
|
||||
<label>
|
||||
Rayon : <span>{kmFilter} km</span>
|
||||
|
|
@ -86,7 +51,12 @@ export default function PostList() {
|
|||
{error && <p className="status-text status-error">{error}</p>}
|
||||
|
||||
{filtered.map((p) => (
|
||||
<PostCard key={p.id ?? p._id} post={p} />
|
||||
<PostCard
|
||||
key={p.id ?? p._id}
|
||||
post={p}
|
||||
selected={selectedPost && (selectedPost.id ?? selectedPost._id) === (p.id ?? p._id)}
|
||||
onSelect={onSelectPost}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!loading && filtered.length === 0 && (
|
||||
|
|
|
|||
11
src/main.jsx
11
src/main.jsx
|
|
@ -1,7 +1,14 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./styles/global.css";
|
||||
import App from "./App.jsx";
|
||||
|
||||
import "./styles/base.css";
|
||||
import "./styles/layout.css";
|
||||
import "./styles/topbar.css";
|
||||
import "./styles/map.css";
|
||||
import "./styles/markers.css";
|
||||
import "./styles/popup.css";
|
||||
import "./styles/overlays.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
/* ========== BASE / RESET ========== */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #050816;
|
||||
color: #ffffff;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
|
|
@ -1,164 +1,284 @@
|
|||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* RESET */
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
background: #050816;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
.app-root {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
/* TOPBAR */
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
gap: 0.6rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: #0b1020;
|
||||
border-bottom: 1px solid #1f2933;
|
||||
border-bottom: 1px solid #1f2937;
|
||||
}
|
||||
|
||||
.topbar-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
.logo-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: radial-gradient(circle, #38bdf8, #1d4ed8, #020617);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
.title-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.main-title { font-size: 1rem; font-weight: 700; }
|
||||
.sub-title { font-size: 0.75rem; opacity: 0.7; }
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.35rem 0.8rem;
|
||||
padding: 0.25rem 0.7rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #2563eb;
|
||||
background: #1d4ed8;
|
||||
color: white;
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid #22c55e;
|
||||
background: #16a34a;
|
||||
color: #f9fafb;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
/* ROOT & LAYOUT */
|
||||
.app-root {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
.main-shell {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 380px) minmax(0, 1fr);
|
||||
gap: 0.75rem;
|
||||
height: calc(100vh - 56px - 1.5rem);
|
||||
}
|
||||
|
||||
.layout-left {
|
||||
background: #0b1020;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.layout-right {
|
||||
background: #020617;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Post list + slider */
|
||||
.post-list {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.post-list h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.km-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.km-filter label {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.km-filter span {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.km-filter input[type="range"] {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.post-card {
|
||||
border-radius: 0.6rem;
|
||||
padding: 0.6rem;
|
||||
background: #111827;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* MAP FULLSCREEN */
|
||||
.map-shell {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid #1f2937;
|
||||
background: #020617;
|
||||
}
|
||||
|
||||
.post-card h3 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.post-card p {
|
||||
margin: 0 0 0.4rem 0;
|
||||
font-size: 0.85rem;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.post-km {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: #f97373;
|
||||
}
|
||||
|
||||
/* MapLibre */
|
||||
.map-view {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid #1f2937;
|
||||
}
|
||||
|
||||
.maplibre-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Overlays autour de la map */
|
||||
.map-overlay {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.map-overlay * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Haut */
|
||||
.top-bar {
|
||||
top: 0.4rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0 0.6rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.lookat-box {
|
||||
padding: 0.25rem 0.6rem;
|
||||
background: rgba(0,0,0,0.6);
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.wire-title { font-size: 0.7rem; opacity: 0.7; }
|
||||
.zoom-buttons { display: flex; gap: 0.3rem; }
|
||||
|
||||
/* Gauche */
|
||||
.left-bar {
|
||||
left: 0.4rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
/* Droite */
|
||||
.right-bar {
|
||||
right: 0.4rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
/* Bas */
|
||||
.bottom-bar {
|
||||
bottom: 0.4rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
/* Chips */
|
||||
.chip-round,
|
||||
.chip-pill,
|
||||
.chip-egg,
|
||||
.chip-hex {
|
||||
background: rgba(20,25,50,0.9);
|
||||
border: 1px solid #1d4ed8;
|
||||
padding: 0.35rem 0.6rem;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.6);
|
||||
}
|
||||
.chip-egg { border-radius: 999px; }
|
||||
.chip-hex { border-radius: 0.4rem; }
|
||||
.chip-pill { border-radius: 999px; }
|
||||
|
||||
/* Status en bas à gauche */
|
||||
.map-status {
|
||||
position: absolute;
|
||||
left: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
background: rgba(0,0,0,0.6);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* === MARKERS (petits posts) === */
|
||||
.post-marker {
|
||||
position: relative;
|
||||
background: rgba(15, 23, 42, 0.96);
|
||||
border-radius: 999px;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.7);
|
||||
cursor: pointer;
|
||||
}
|
||||
.post-marker::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -7px;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: 7px 7px 0 7px;
|
||||
border-style: solid;
|
||||
border-color: rgba(15, 23, 42, 0.96) transparent transparent transparent;
|
||||
}
|
||||
.post-marker-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.post-marker-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 140px;
|
||||
}
|
||||
.post-marker-title {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.post-marker-snippet {
|
||||
font-size: 0.65rem;
|
||||
opacity: 0.8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.post-marker-image {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(148, 163, 184, 0.8);
|
||||
}
|
||||
.post-marker-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.post-marker-image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at 30% 30%, #38bdf8, #1d4ed8, #020617);
|
||||
}
|
||||
|
||||
/* === POPUP GROS POST === */
|
||||
.maplibregl-popup-content {
|
||||
padding: 0;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.popup-post-card {
|
||||
background: rgba(15, 23, 42, 0.97);
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.85);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.popup-post-inner {
|
||||
padding: 0.65rem 0.75rem 0.75rem 0.75rem;
|
||||
}
|
||||
.popup-post-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.popup-post-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||
}
|
||||
.popup-post-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.popup-post-image-placeholder {
|
||||
background: radial-gradient(circle at 30% 30%, #38bdf8, #1d4ed8, #020617);
|
||||
}
|
||||
.popup-post-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.popup-post-body {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.35;
|
||||
color: #cbd5f5;
|
||||
max-height: 190px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/* ========== LAYOUT GLOBAL (structure) ========== */
|
||||
|
||||
.app-root {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-shell {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
/* Conteneur de la carte plein écran */
|
||||
.map-shell {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid #1f2937;
|
||||
background: #020617;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/* ========== CARTE / CONTAINER MAPLIBRE ========== */
|
||||
|
||||
.map-view {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-container,
|
||||
.maplibre-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Petit statut en bas à gauche (ex: "Loading posts...") */
|
||||
.map-status {
|
||||
position: absolute;
|
||||
left: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/* ========== MARKERS (PETITS & GROS POSTS SUR LA MAP) ========== */
|
||||
|
||||
/* base commune */
|
||||
.post-marker {
|
||||
position: relative;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
cursor: pointer;
|
||||
transform: translateY(0); /* important pour garder le même ancrage */
|
||||
}
|
||||
|
||||
/* --- PETIT POST (COMPACT) --- */
|
||||
.post-marker-compact {
|
||||
background: rgba(0, 0, 0, 0.78);
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(15, 23, 42, 0.7),
|
||||
0 6px 18px rgba(0, 0, 0, 0.8),
|
||||
0 0 16px rgba(56, 189, 248, 0.4);
|
||||
}
|
||||
|
||||
.post-marker-compact::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -7px;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 7px 7px 0 7px;
|
||||
border-color: rgba(0, 0, 0, 0.78) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.marker-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.marker-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 30% 30%, #4ade80, #22c55e, #0284c7);
|
||||
}
|
||||
|
||||
.marker-text {
|
||||
max-width: 140px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* --- GROS POST (EXPANDED) --- */
|
||||
.post-marker-expanded {
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
padding: 8px 10px 10px 10px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||
box-shadow:
|
||||
0 10px 30px rgba(0, 0, 0, 0.9),
|
||||
0 0 24px rgba(56, 189, 248, 0.55);
|
||||
}
|
||||
|
||||
.post-marker-expanded::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -10px;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 10px 10px 0 10px;
|
||||
border-color: rgba(15, 23, 42, 0.98) transparent transparent transparent;
|
||||
}
|
||||
|
||||
/* contenu du gros post */
|
||||
.marker-expanded-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
max-width: 230px;
|
||||
}
|
||||
|
||||
.marker-expanded-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.marker-expanded-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.marker-expanded-body {
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
opacity: 0.95;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
/* ==========================================
|
||||
SOCIOWIRE — OVERLAY + ARC AUTOUR DU GLOBE
|
||||
========================================== */
|
||||
|
||||
.map-overlay {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.map-overlay * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* --------- TOP --------- */
|
||||
|
||||
.map-overlay-top {
|
||||
top: 0.7rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lookat-box {
|
||||
padding: 0.35rem 0.9rem;
|
||||
background: rgba(0, 10, 30, 0.85);
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid rgba(56, 189, 248, 0.5);
|
||||
color: #dbeafe;
|
||||
}
|
||||
|
||||
.wire-title {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.9;
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.zoom-buttons {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
/* --------- LEFT --------- */
|
||||
|
||||
.map-overlay-left {
|
||||
left: 0.7rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* --------- RIGHT --------- */
|
||||
|
||||
.map-overlay-right {
|
||||
right: 0.7rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Bouton principal sélectionné (News/Friends/Events/Market) */
|
||||
.chip-selected {
|
||||
background: linear-gradient(135deg, #2563eb, #0ea5e9);
|
||||
border-color: #7dd3fc;
|
||||
box-shadow: 0 0 20px rgba(56, 189, 248, 0.55);
|
||||
}
|
||||
|
||||
/* --------- BOTTOM — ARC (type orbite) --------- */
|
||||
|
||||
.map-overlay-bottom {
|
||||
bottom: 3.2rem; /* au-dessus de la barre Android */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
/* On crée un arc en jouant avec translateY + rotate */
|
||||
.map-overlay-bottom .chip-egg:nth-child(1) {
|
||||
transform: translateY(8px) rotate(-12deg);
|
||||
}
|
||||
.map-overlay-bottom .chip-egg:nth-child(2) {
|
||||
transform: translateY(4px) rotate(-6deg);
|
||||
}
|
||||
.map-overlay-bottom .chip-egg:nth-child(3) {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
.map-overlay-bottom .chip-egg:nth-child(4) {
|
||||
transform: translateY(4px) rotate(6deg);
|
||||
}
|
||||
.map-overlay-bottom .chip-egg:nth-child(5) {
|
||||
transform: translateY(8px) rotate(12deg);
|
||||
}
|
||||
|
||||
/* --------- CHIPS (boutons) --------- */
|
||||
|
||||
.chip-round,
|
||||
.chip-pill,
|
||||
.chip-egg,
|
||||
.chip-hex {
|
||||
background: rgba(15, 23, 42, 0.96);
|
||||
border: 1px solid rgba(148, 163, 184, 0.85);
|
||||
color: #e5e7eb;
|
||||
padding: 0.45rem 0.9rem;
|
||||
font-size: 0.78rem;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 5px 12px rgba(0, 0, 0, 0.7),
|
||||
0 0 12px rgba(56, 189, 248, 0.35);
|
||||
}
|
||||
|
||||
/* boutons d’orbite (catégories dynamiques) */
|
||||
.chip-egg {
|
||||
background: rgba(0, 10, 30, 0.9);
|
||||
border: 1px solid rgba(56, 189, 248, 0.55);
|
||||
}
|
||||
|
||||
/* catégorie sélectionnée (première) */
|
||||
.chip-egg-active {
|
||||
background: linear-gradient(135deg, #1d4ed8, #0ea5e9);
|
||||
border-color: #7dd3fc;
|
||||
}
|
||||
|
||||
/* boutons hex (Zoom, Swall) */
|
||||
.chip-hex {
|
||||
border-radius: 0.65rem;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/* ========== POPUP (GROS POST AU CLIC) ========== */
|
||||
|
||||
.maplibregl-popup-content {
|
||||
padding: 0;
|
||||
border-radius: 16px;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* conteneur du gros post */
|
||||
.popup-post-card {
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||
box-shadow:
|
||||
0 10px 30px rgba(0, 0, 0, 0.9),
|
||||
0 0 26px rgba(56, 189, 248, 0.55);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* crochet du gros post */
|
||||
.popup-post-card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -10px;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 10px 10px 0 10px;
|
||||
border-color: rgba(15, 23, 42, 0.98) transparent transparent transparent;
|
||||
}
|
||||
|
||||
.popup-post-inner {
|
||||
padding: 8px 10px 10px 10px;
|
||||
}
|
||||
|
||||
.popup-post-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* petit rond bleu dans le gros post, pour garder le lien visuel */
|
||||
.popup-post-dot {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
background: radial-gradient(circle at 30% 30%, #4ade80, #22c55e, #0284c7);
|
||||
}
|
||||
|
||||
.popup-post-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.popup-post-body {
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.popup-post-empty {
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/* ========== TOP BAR (SOCIOWIRE header) ========== */
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.6rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: #0b1020;
|
||||
border-bottom: 1px solid #1f2937;
|
||||
}
|
||||
|
||||
.logo-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: radial-gradient(circle, #38bdf8, #1d4ed8, #020617);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.title-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.25rem 0.7rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #22c55e;
|
||||
background: #16a34a;
|
||||
color: #f9fafb;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -1,7 +1,17 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
// https://vite.dev/config/
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 5173,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:8081", // ton backend
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue