tout fonctionne logins et post correct
This commit is contained in:
parent
48d5ed01a0
commit
d2bde41aa4
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"keycloak-js": "^26.2.1",
|
||||||
"maplibre-gl": "^5.14.0",
|
"maplibre-gl": "^5.14.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|
@ -2200,6 +2201,15 @@
|
||||||
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/keycloak-js": {
|
||||||
|
"version": "26.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.1.tgz",
|
||||||
|
"integrity": "sha512-bZt6fQj/TLBAmivXSxSlqAJxBx/knNZDQGJIW4ensGYGN4N6tUKV8Zj3Y7/LOV8eIpvWsvqV70fbACihK8Ze0Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"workspaces": [
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"keycloak-js": "^26.2.1",
|
||||||
"maplibre-gl": "^5.14.0",
|
"maplibre-gl": "^5.14.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { Suspense, useEffect, useState } from "react";
|
||||||
import TopBar from "./components/Layout/TopBar";
|
import TopBar from "./components/Layout/TopBar";
|
||||||
import MapView from "./components/Map/MapView";
|
|
||||||
|
// Lazy-load de la carte (gros chunk : maplibre, CSS, etc.)
|
||||||
|
const MapView = React.lazy(() => import("./components/Map/MapView"));
|
||||||
|
|
||||||
const THEME_KEY = "sociowire:theme";
|
const THEME_KEY = "sociowire:theme";
|
||||||
|
|
||||||
|
|
@ -39,8 +41,9 @@ export default function App() {
|
||||||
<TopBar theme={theme} onChangeTheme={setTheme} />
|
<TopBar theme={theme} onChangeTheme={setTheme} />
|
||||||
<div className="main-shell">
|
<div className="main-shell">
|
||||||
<div className="map-shell">
|
<div className="map-shell">
|
||||||
{/* on passe le thème à la carte */}
|
<Suspense fallback={<div style={{ padding: 16 }}>Loading map…</div>}>
|
||||||
<MapView theme={theme} />
|
<MapView theme={theme} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,6 @@
|
||||||
|
|
||||||
const API_BASE = "/api";
|
const API_BASE = "/api";
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère la liste des posts avec filtres.
|
|
||||||
* filters: {
|
|
||||||
* category?: "NEWS" | "FRIENDS" | "EVENT" | "MARKET"
|
|
||||||
* subCategory?: string
|
|
||||||
* time?: "NOW" | "TODAY" | "RECENT" | "PAST"
|
|
||||||
* lat?: number
|
|
||||||
* lon?: number
|
|
||||||
* radiusKm?: number
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
export async function fetchPosts(filters = {}) {
|
export async function fetchPosts(filters = {}) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
|
@ -45,7 +34,6 @@ export async function fetchPosts(filters = {}) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère la position approx de l'utilisateur depuis le backend.
|
* Récupère la position approx de l'utilisateur depuis le backend.
|
||||||
* Backend: GET /api/ip-location → { lat: number, lon: number } ou "null"
|
|
||||||
*/
|
*/
|
||||||
export async function fetchIpLocation() {
|
export async function fetchIpLocation() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -93,15 +81,19 @@ export async function fetchIpLocation() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée un nouveau post
|
* Crée un nouveau post
|
||||||
* Backend: POST /api/post (postCreateHandler)
|
* token: access token Keycloak (string)
|
||||||
* Attend: { title, snippet, category, sub_category, lat, lon, author }
|
|
||||||
*/
|
*/
|
||||||
export async function createPost(payload) {
|
export async function createPost(payload, token) {
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
if (token) {
|
||||||
|
headers["Authorization"] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/post`, {
|
const res = await fetch(`${API_BASE}/post`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers,
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
import keycloak from "./keycloak";
|
||||||
|
|
||||||
|
const AuthContext = createContext(null);
|
||||||
|
|
||||||
|
export function AuthProvider({ children }) {
|
||||||
|
const [kc, setKc] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [authenticated, setAuthenticated] = useState(false);
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [lastError, setLastError] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
console.log("[AUTH] init keycloak…");
|
||||||
|
const ok = await keycloak.init({
|
||||||
|
onLoad: "check-sso", // ne force pas le login au chargement
|
||||||
|
checkLoginIframe: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isMounted) return;
|
||||||
|
|
||||||
|
console.log("[AUTH] init result:", ok);
|
||||||
|
window.kc = keycloak; // debug global
|
||||||
|
|
||||||
|
setKc(keycloak);
|
||||||
|
setAuthenticated(ok);
|
||||||
|
|
||||||
|
if (ok && keycloak.tokenParsed) {
|
||||||
|
const p = keycloak.tokenParsed;
|
||||||
|
const name =
|
||||||
|
p.preferred_username ||
|
||||||
|
p.email ||
|
||||||
|
p.name ||
|
||||||
|
(p.sub ? String(p.sub) : "");
|
||||||
|
setUsername(name || "");
|
||||||
|
} else {
|
||||||
|
setUsername("");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[AUTH] keycloak init error", err);
|
||||||
|
if (isMounted) {
|
||||||
|
setLastError(String(err));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
// refresh token simplifié
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (!keycloak || !keycloak.authenticated) return;
|
||||||
|
keycloak
|
||||||
|
.updateToken(60)
|
||||||
|
.then((refreshed) => {
|
||||||
|
if (refreshed) {
|
||||||
|
console.log("[AUTH] token refreshed");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn("[AUTH] token refresh error", err);
|
||||||
|
});
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const login = () => {
|
||||||
|
keycloak.login({
|
||||||
|
redirectUri: window.location.href,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
keycloak.logout({
|
||||||
|
redirectUri: window.location.origin + "/",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
keycloak: kc,
|
||||||
|
loading,
|
||||||
|
authenticated,
|
||||||
|
username,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
lastError,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const ctx = useContext(AuthContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("useAuth must be used inside <AuthProvider>");
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import keycloak from "./keycloak";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise Keycloak sans forcer le login.
|
||||||
|
* - onLoad: "check-sso" => ne redirige pas si user non loggé
|
||||||
|
*/
|
||||||
|
export function initAuth() {
|
||||||
|
return keycloak.init({
|
||||||
|
onLoad: "check-sso",
|
||||||
|
checkLoginIframe: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appelle une action qui nécessite d'être authentifié.
|
||||||
|
* Si non loggé => ouvre la page de login Keycloak, puis revient sur la même page.
|
||||||
|
*/
|
||||||
|
export function requireAuth(action) {
|
||||||
|
if (keycloak.authenticated) {
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
|
||||||
|
keycloak.login({
|
||||||
|
redirectUri: window.location.href,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le token JWT actuel (ou null)
|
||||||
|
*/
|
||||||
|
export function getToken() {
|
||||||
|
return keycloak.token || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le username depuis le token (si présent)
|
||||||
|
*/
|
||||||
|
export function getUsername() {
|
||||||
|
const profile = keycloak.tokenParsed || {};
|
||||||
|
return (
|
||||||
|
profile.preferred_username ||
|
||||||
|
profile.email ||
|
||||||
|
profile.name ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout et revient sur la même page.
|
||||||
|
*/
|
||||||
|
export function logout() {
|
||||||
|
keycloak.logout({
|
||||||
|
redirectUri: window.location.origin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default keycloak;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import Keycloak from "keycloak-js";
|
||||||
|
|
||||||
|
const keycloak = new Keycloak({
|
||||||
|
url: "https://web.sociowire.com:8443",
|
||||||
|
realm: "sociowire",
|
||||||
|
clientId: "sociowire-frontend",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default keycloak;
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useAuth } from "../../auth/AuthContext";
|
||||||
|
|
||||||
const THEME_LABELS = {
|
const THEME_LABELS = {
|
||||||
dark: "Dark",
|
dark: "Dark",
|
||||||
|
|
@ -7,6 +8,12 @@ const THEME_LABELS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TopBar({ theme = "dark", onChangeTheme }) {
|
export default function TopBar({ theme = "dark", onChangeTheme }) {
|
||||||
|
const { loading, authenticated, username, login, logout, lastError } = useAuth();
|
||||||
|
|
||||||
|
let authLabel = "AUTH: anon";
|
||||||
|
if (loading) authLabel = "AUTH: loading…";
|
||||||
|
else if (authenticated) authLabel = `AUTH: user=${username || "?"}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div className="logo-circle">SW</div>
|
<div className="logo-circle">SW</div>
|
||||||
|
|
@ -14,6 +21,14 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
|
||||||
<div className="title-block">
|
<div className="title-block">
|
||||||
<div className="main-title">SOCIOWIRE.com</div>
|
<div className="main-title">SOCIOWIRE.com</div>
|
||||||
<div className="sub-title">Wired to life</div>
|
<div className="sub-title">Wired to life</div>
|
||||||
|
<div style={{ fontSize: "0.65rem", opacity: 0.7 }}>
|
||||||
|
{authLabel}
|
||||||
|
{lastError && (
|
||||||
|
<span style={{ color: "#f97373", marginLeft: "0.3rem" }}>
|
||||||
|
(auth err)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="topbar-right">
|
<div className="topbar-right">
|
||||||
|
|
@ -32,7 +47,23 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-primary">Login</button>
|
|
||||||
|
{loading ? (
|
||||||
|
<button className="btn-primary" disabled>
|
||||||
|
…
|
||||||
|
</button>
|
||||||
|
) : authenticated ? (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
|
||||||
|
<span style={{ fontSize: "0.8rem" }}>{username}</span>
|
||||||
|
<button className="btn-primary" onClick={logout}>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button className="btn-primary" onClick={login}>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,11 @@ import { useMapCore } from "./useMapCore";
|
||||||
import { useUserPosition } from "./useUserPosition";
|
import { useUserPosition } from "./useUserPosition";
|
||||||
import { usePostsEngine } from "./usePostsEngine";
|
import { usePostsEngine } from "./usePostsEngine";
|
||||||
import PostList from "../Posts/PostList";
|
import PostList from "../Posts/PostList";
|
||||||
|
import { useAuth } from "../../auth/AuthContext";
|
||||||
|
|
||||||
export default function MapView({ theme = "dark" }) {
|
export default function MapView({ theme = "dark" }) {
|
||||||
|
const { authenticated, username } = useAuth();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
containerRef,
|
containerRef,
|
||||||
mapRef,
|
mapRef,
|
||||||
|
|
@ -69,6 +72,7 @@ export default function MapView({ theme = "dark" }) {
|
||||||
}
|
}
|
||||||
}, [mainFilter, bottomCategories, subFilter]);
|
}, [mainFilter, bottomCategories, subFilter]);
|
||||||
|
|
||||||
|
// WebSocket live
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
const proto = window.location.protocol === "https:" ? "wss" : "ws";
|
||||||
const host =
|
const host =
|
||||||
|
|
@ -102,6 +106,10 @@ export default function MapView({ theme = "dark" }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenCreate = () => {
|
const handleOpenCreate = () => {
|
||||||
|
if (!authenticated) {
|
||||||
|
alert("You must be logged in to place a wire.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
setDraftTitle("");
|
setDraftTitle("");
|
||||||
setDraftBody("");
|
setDraftBody("");
|
||||||
|
|
@ -121,26 +129,76 @@ export default function MapView({ theme = "dark" }) {
|
||||||
setSaveError("");
|
setSaveError("");
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
let coords = draftCoords || [map.getCenter().lng, map.getCenter().lat];
|
|
||||||
const [lng, lat] = coords;
|
if (!authenticated) {
|
||||||
|
setSaveError("You must be logged in to post.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorName = (username || "").trim() || "anon";
|
||||||
|
|
||||||
|
// ----- CROSSHAIR OFFSET FIX -----
|
||||||
|
// Point de départ : soit coords définies, soit centre de la map
|
||||||
|
let baseLng;
|
||||||
|
let baseLat;
|
||||||
|
|
||||||
|
if (draftCoords && draftCoords.length === 2) {
|
||||||
|
baseLng = draftCoords[0];
|
||||||
|
baseLat = draftCoords[1];
|
||||||
|
} else {
|
||||||
|
const center = map.getCenter();
|
||||||
|
baseLng = center.lng;
|
||||||
|
baseLat = center.lat;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset vertical en pixels :
|
||||||
|
// si le post apparaît trop bas, on remonte (pixelOffsetY négatif)
|
||||||
|
const pixelOffsetY = -20; // ajuste à -25/-30 si nécessaire
|
||||||
|
|
||||||
|
// lng/lat -> pixels
|
||||||
|
const screenPoint = map.project([baseLng, baseLat]);
|
||||||
|
|
||||||
|
// appliquer l'offset vertical
|
||||||
|
const correctedPoint = {
|
||||||
|
x: screenPoint.x,
|
||||||
|
y: screenPoint.y + pixelOffsetY,
|
||||||
|
};
|
||||||
|
|
||||||
|
// pixels -> lng/lat corrigé
|
||||||
|
const correctedLngLat = map.unproject([correctedPoint.x, correctedPoint.y]);
|
||||||
|
const lng = correctedLngLat.lng;
|
||||||
|
const lat = correctedLngLat.lat;
|
||||||
|
// ----- END FIX -----
|
||||||
|
|
||||||
if (!draftTitle.trim()) {
|
if (!draftTitle.trim()) {
|
||||||
setSaveError("Title required.");
|
setSaveError("Title required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
await createPost({
|
|
||||||
|
// On récupère le post retourné par le backend
|
||||||
|
const newPost = await createPost({
|
||||||
title: draftTitle.trim(),
|
title: draftTitle.trim(),
|
||||||
snippet: draftBody.trim() || draftTitle.trim(),
|
snippet: draftBody.trim() || draftTitle.trim(),
|
||||||
category: draftCategory.toUpperCase(),
|
category: draftCategory.toUpperCase(),
|
||||||
sub_category: draftSubCategory || "ALL",
|
sub_category: draftSubCategory || "ALL",
|
||||||
lat,
|
lat,
|
||||||
lon: lng,
|
lon: lng,
|
||||||
author: "tommy",
|
author: authorName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// On l’injecte direct dans le moteur de posts,
|
||||||
|
// en plus du broadcast WebSocket
|
||||||
|
if (newPost && newPost.id) {
|
||||||
|
handleIncomingPost(newPost);
|
||||||
|
}
|
||||||
|
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
setSaveError("Error creating post.");
|
setSaveError("Error creating post.");
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +291,14 @@ export default function MapView({ theme = "dark" }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CREATE POST */}
|
{/* CROSSHAIR au centre pendant la création */}
|
||||||
|
{isCreating && (
|
||||||
|
<div className="map-overlay map-crosshair">
|
||||||
|
<div className="crosshair-aim" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* CREATE POST PANEL */}
|
||||||
{isCreating && (
|
{isCreating && (
|
||||||
<div className="map-overlay create-post-panel">
|
<div className="map-overlay create-post-panel">
|
||||||
<div className="create-post-header">
|
<div className="create-post-header">
|
||||||
|
|
@ -246,7 +311,8 @@ export default function MapView({ theme = "dark" }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span className="crosshair-label">
|
<span className="crosshair-label">
|
||||||
Move the map, then tap to fix the position.
|
Move the map, aim with the crosshair — your wire will be pinned
|
||||||
|
there.
|
||||||
</span>
|
</span>
|
||||||
<div className="create-row">
|
<div className="create-row">
|
||||||
<select
|
<select
|
||||||
|
|
@ -293,7 +359,7 @@ export default function MapView({ theme = "dark" }) {
|
||||||
onClick={handleSubmitPost}
|
onClick={handleSubmitPost}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
>
|
>
|
||||||
{isSaving ? "Posting..." : "Post as tommy"}
|
{isSaving ? "Posting..." : "Post"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,12 @@ import "./styles/overlays.css";
|
||||||
import "./styles/posts.css";
|
import "./styles/posts.css";
|
||||||
import "./styles/theme.css";
|
import "./styles/theme.css";
|
||||||
|
|
||||||
|
import { AuthProvider } from "./auth/AuthContext.jsx";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<AuthProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</AuthProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue