diff --git a/package-lock.json b/package-lock.json
index 41eddd0..b30d161 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "sociowire-frontend",
- "version": "0.0.28",
+ "version": "0.0.64",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sociowire-frontend",
- "version": "0.0.28",
+ "version": "0.0.64",
"dependencies": {
"@fortawesome/fontawesome-free": "^7.1.0",
"axios": "^1.13.2",
diff --git a/package.json b/package.json
index 9dc0bd3..f200536 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "sociowire-frontend",
"private": true,
- "version": "0.0.52",
+ "version": "0.0.64",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
diff --git a/src/api/client.js b/src/api/client.js
index 62a89e4..0114352 100644
--- a/src/api/client.js
+++ b/src/api/client.js
@@ -386,34 +386,24 @@ export async function fetchPostTruth(postId) {
if (!res.ok) return null;
const data = await res.json().catch(() => null);
if (!data) return null;
- // Normalize response - API may return { truth: {...} } or flat object
+ // Normalize response - API returns { truth: {...} }
const truth = data?.truth || data || {};
- const voteCount = truth.vote_count ?? truth.total_votes ?? truth.count ?? 0;
- const aiScore = truth.ai_score ?? truth.ai ?? null;
- const userScore = truth.user_score ?? truth.user ?? null;
- // Separate truth_score field (only if not already captured by ai/user)
- const explicitTruth = aiScore === null && userScore === null ? (truth.truth_score ?? null) : null;
- // Only return if we have actual meaningful data:
- // - Must have votes > 0, OR
- // - Must have an AI score that's explicitly set (not 0 with no votes)
- const hasAI = aiScore !== null && Number.isFinite(aiScore);
- const hasUser = userScore !== null && Number.isFinite(userScore);
- const hasExplicit = explicitTruth !== null && Number.isFinite(explicitTruth);
+ // truth_score is the main combined score (0-100)
+ const truthScore = truth.truth_score ?? null;
+ const aiScore = truth.ai_score ?? null;
+ const voteCount = truth.total_votes ?? truth.vote_count ?? 0;
- // If no real score data, don't override the initial calculated score
- if (!hasAI && !hasUser && !hasExplicit) return null;
- // If score is 0 with no votes, it's likely uninitialized - don't update
- const mainScore = hasUser ? userScore : (hasAI ? aiScore : explicitTruth);
- if (mainScore === 0 && voteCount === 0 && !hasAI) return null;
+ // Return null only if we have no data at all
+ if (truthScore === null && aiScore === null) return null;
return {
- ai_score: hasAI ? aiScore : null,
- user_score: hasUser ? userScore : null,
- truth_score: mainScore, // Main display score - no fallback to 50
+ truth_score: truthScore ?? 50, // Main display score
+ ai_score: aiScore,
+ user_score: truthScore, // For backward compatibility
total_votes: voteCount,
vote_count: voteCount,
- user_vote: truth.user_vote ?? truth.UserVote ?? null,
+ user_vote: truth.user_vote ?? null,
};
} catch {
return null;
@@ -1160,8 +1150,8 @@ export async function changePassword(oldPassword, newPassword) {
export async function uploadPostMedia(file, opts = {}) {
if (!file) return { ok: false, error: "No file provided" };
- const mediaBase = "https://media.sociowire.com";
- const url = `${mediaBase}/api/upload`;
+ // Use main API for uploads (asset-service proxy)
+ const url = `${apiBase()}/assets/upload`;
const form = new FormData();
form.append("file", file);
diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx
index 37fa249..20dff43 100644
--- a/src/components/Map/MapView.jsx
+++ b/src/components/Map/MapView.jsx
@@ -405,7 +405,8 @@ export default function MapView({
longPressTimerRef.current = setTimeout(() => {
if (longPressStartPos.current && longPressCallbackRef.current) {
- longPressCallbackRef.current();
+ // Pass the finger position to the callback
+ longPressCallbackRef.current(longPressStartPos.current);
}
cancelLongPress();
}, LONG_PRESS_DURATION);
@@ -438,7 +439,8 @@ export default function MapView({
longPressStartPos.current = { x: e.clientX, y: e.clientY };
longPressTimerRef.current = setTimeout(() => {
if (longPressStartPos.current && longPressCallbackRef.current) {
- longPressCallbackRef.current();
+ // Pass the finger/mouse position to the callback
+ longPressCallbackRef.current(longPressStartPos.current);
}
cancelLongPress();
}, LONG_PRESS_DURATION);
@@ -1504,7 +1506,7 @@ export default function MapView({
setCreateStep((prev) => Math.max(0, prev - 1));
};
- const handleOpenCreate = () => {
+ const handleOpenCreate = (fingerPos = null) => {
if (!authenticated) {
try {
window.dispatchEvent(
@@ -1534,6 +1536,32 @@ export default function MapView({
return;
}
+ // Pan map so crosshair aligns with finger position
+ const map = mapRef.current;
+ if (map && fingerPos) {
+ const container = map.getContainer();
+ if (container) {
+ const rect = container.getBoundingClientRect();
+ // Convert finger screen position to map container position
+ const fingerX = fingerPos.x - rect.left;
+ const fingerY = fingerPos.y - rect.top;
+
+ // Get geo coords at finger position
+ const fingerCoords = map.unproject([fingerX, fingerY]);
+
+ // Crosshair is at center X, 45% from top Y
+ const crosshairX = rect.width / 2;
+ const crosshairY = rect.height * 0.45;
+
+ // Calculate pixel offset from crosshair to finger
+ const offsetX = fingerX - crosshairX;
+ const offsetY = fingerY - crosshairY;
+
+ // Pan the map by this offset so the finger location ends up at crosshair
+ map.panBy([offsetX, offsetY], { duration: 300 });
+ }
+ }
+
// Step 1: Show crosshair to pick location
setSaveError("");
setIsSaving(false);
@@ -1542,24 +1570,41 @@ export default function MapView({
};
// Confirm location and open modal
- const handleConfirmLocation = () => {
+ // Get coordinates from crosshair position (reusable)
+ const getCrosshairCoords = useCallback(() => {
const map = mapRef.current;
- if (map) {
- const stage = stageRef.current;
- const crosshairEl = stage?.querySelector(".crosshair-aim");
- const containerEl = map.getContainer();
- let coords;
- if (crosshairEl && containerEl) {
- const cr = crosshairEl.getBoundingClientRect();
- const co = containerEl.getBoundingClientRect();
- const ll = map.unproject([cr.left + cr.width/2 - co.left, cr.top + cr.height/2 - co.top]);
- coords = [ll.lng, ll.lat];
- } else {
- const c = map.getCenter();
- coords = [c.lng, c.lat];
- }
- setDraftCoords(coords);
+ if (!map) return null;
+
+ const container = map.getContainer();
+ if (!container) return null;
+
+ const rect = container.getBoundingClientRect();
+ // Crosshair position: center X, 45% from top Y (thumb-friendly position)
+ const x = rect.width / 2;
+ const y = rect.height * 0.45;
+ const ll = map.unproject([x, y]);
+
+ if (ll && Number.isFinite(ll.lng) && Number.isFinite(ll.lat)) {
+ return [ll.lng, ll.lat];
}
+
+ // Fallback to map center
+ const c = map.getCenter();
+ return [c.lng, c.lat];
+ }, [mapRef]);
+
+ const handleConfirmLocation = () => {
+ let coords = getCrosshairCoords();
+
+ // Validate coords - never allow 0,0 or invalid values
+ if (!coords || !Array.isArray(coords) || coords.length !== 2 ||
+ !Number.isFinite(coords[0]) || !Number.isFinite(coords[1]) ||
+ (coords[0] === 0 && coords[1] === 0)) {
+ // Use a safe default (will be overwritten by map center in onSubmit)
+ coords = null;
+ }
+
+ setDraftCoords(coords);
setIsPickingLocation(false);
setIsCreating(true);
};
@@ -1569,6 +1614,26 @@ export default function MapView({
setDraftCoords(null);
};
+ // Update coordinates when map moves during post creation (step 2)
+ useEffect(() => {
+ const map = mapRef.current;
+ if (!map || !isCreating) return;
+
+ const updateCoords = () => {
+ const coords = getCrosshairCoords();
+ if (coords && coords[0] !== 0 && coords[1] !== 0) {
+ setDraftCoords(coords);
+ }
+ };
+
+ // Update coords when map moves
+ map.on('moveend', updateCoords);
+
+ return () => {
+ map.off('moveend', updateCoords);
+ };
+ }, [isCreating, getCrosshairCoords]);
+
// Update ref for long-press callback
useEffect(() => {
longPressCallbackRef.current = handleOpenCreate;
@@ -2160,27 +2225,29 @@ export default function MapView({
weatherEnabled={weatherViewEnabled}
/>
- {/* Step 1: Location Picker with Crosshair */}
+ {/* Crosshair - shown during location picking AND while creating post */}
+ {(isPickingLocation || isCreating) && (
+
+ )}
+
+ {/* Step 1: Location Picker UI (buttons/hint) */}
{isPickingLocation && (
- <>
-
-
+
+
+
+ Move the map to position your post
-
-
-
- Move the map to position your post
-
-
-
-
-
+
+
+
- >
+
)}
{/* Step 2: Post Creation Modal */}
@@ -2193,30 +2260,49 @@ export default function MapView({
try {
const map = mapRef.current;
let coords = data.coords;
- if (!coords && map) {
- const c = map.getCenter();
- coords = [c.lng, c.lat];
+
+ // Ensure we have valid coordinates - never allow 0,0
+ if (!coords || !Array.isArray(coords) || coords.length !== 2 ||
+ !Number.isFinite(coords[0]) || !Number.isFinite(coords[1])) {
+ if (map) {
+ const c = map.getCenter();
+ coords = [c.lng, c.lat];
+ }
}
+
+ // Final validation - reject 0,0 (null island)
+ const lng = coords?.[0];
+ const lat = coords?.[1];
+ if (!Number.isFinite(lng) || !Number.isFinite(lat) || (lng === 0 && lat === 0)) {
+ setSaveError("Invalid location. Please try again.");
+ setIsSaving(false);
+ return;
+ }
+
+ // Get uploaded images
+ const uploadedImages = Array.isArray(data.images) ? data.images.filter(Boolean) : [];
+
const payload = {
title: data.title,
- body: data.body,
+ snippet: data.body || data.title, // Backend uses 'snippet' not 'body'
category: data.category === "Events" ? "EVENT" : data.category.toUpperCase(),
- sub_category: data.subCategory,
- content_type: data.mode === "photo" ? "photo" : (data.mode === "link" ? "link" : "text"),
- longitude: coords?.[0] ?? 0,
- latitude: coords?.[1] ?? 0,
+ sub_category: data.subCategory || "ALL",
+ content_type: data.mode === "photo" && uploadedImages.length > 0 ? "picture" : (data.mode === "link" ? "link" : "text"),
+ lon: lng, // Backend uses 'lon' not 'longitude'
+ lat: lat, // Backend uses 'lat' not 'latitude'
url: data.linkUrl || "",
- image_small: data.images?.[0] || data.linkPreview?.image || "",
- image_medium: data.images?.[1] || data.images?.[0] || data.linkPreview?.image || "",
- image_large: data.images?.[2] || data.images?.[1] || data.images?.[0] || data.linkPreview?.image || "",
- media_urls: data.images || [],
- source: data.linkPreview?.site_name || "",
+ image_small: uploadedImages[0] || data.linkPreview?.image || "",
+ image_medium: uploadedImages[1] || uploadedImages[0] || data.linkPreview?.image || "",
+ image_large: uploadedImages[2] || uploadedImages[1] || uploadedImages[0] || data.linkPreview?.image || "",
+ media_urls: uploadedImages,
+ source: data.linkPreview?.source || "sociowire",
};
+
const result = await createPost(payload);
if (result?.id) {
+ // Add the new post to the display immediately
+ handleIncomingPost(result);
setIsCreating(false);
- // Refresh posts
- if (typeof refreshPosts === "function") refreshPosts();
}
} catch (err) {
setSaveError(err.message || "Failed to create post");
diff --git a/src/components/Map/markerManager.js b/src/components/Map/markerManager.js
index c718d42..f4c3d13 100644
--- a/src/components/Map/markerManager.js
+++ b/src/components/Map/markerManager.js
@@ -794,9 +794,13 @@ function collectGalleryImages(post) {
seen.add(normalized);
result.push(normalized);
};
+ // Check both media_urls (frontend) and image_urls (backend API)
if (Array.isArray(post?.media_urls)) {
post.media_urls.forEach(add);
}
+ if (Array.isArray(post?.image_urls)) {
+ post.image_urls.forEach(add);
+ }
["image_large", "image_medium", "image_small", "image", "media_url"].forEach((key) => {
add(post?.[key]);
});
@@ -1381,6 +1385,76 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
+
+
+
+
+
+ ${truth}
+ Truth Score
+
+
+
+
+
+
+
👁 ${formatCount(viewCount)}
❤️ ${formatCount(likeCount)}
@@ -1501,6 +1575,76 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
${escapeHtml(data?.summary || postData?.snippet || "")}
+
+
+
+
+
+ ${truth}
+ Truth Score
+
+
+
+
+
+
+
👁 ${formatCount(viewCount)}
❤️ ${formatCount(likeCount)}
@@ -1797,6 +1941,93 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
});
}
+ // Truth vote handlers (thumbs up/down)
+ const truthVoteSection = modalWrap.querySelector('.sw-truth-vote-section');
+ if (truthVoteSection && postID > 0) {
+ const downvoteBtn = truthVoteSection.querySelector('.sw-truth-downvote');
+ const upvoteBtn = truthVoteSection.querySelector('.sw-truth-upvote');
+ const scoreDisplay = truthVoteSection.querySelector('.sw-truth-score-display');
+ const railFill = truthVoteSection.querySelector('.sw-truth-rail-fill');
+ let currentVote = null;
+ let isVoting = false;
+
+ const updateTruthUI = (newScore) => {
+ if (scoreDisplay && Number.isFinite(newScore)) {
+ const score = Math.round(newScore);
+ scoreDisplay.textContent = String(score);
+ // Update color based on score
+ const color = score >= 70 ? '#34d399' : score >= 50 ? '#fbbf24' : '#f87171';
+ const glow = score >= 70 ? 'rgba(52, 211, 153, 0.5)' : score >= 50 ? 'rgba(251, 191, 36, 0.5)' : 'rgba(248, 113, 113, 0.5)';
+ scoreDisplay.style.color = color;
+ scoreDisplay.style.textShadow = `0 0 20px ${glow}`;
+ // Update rail
+ if (railFill) {
+ railFill.style.width = `${Math.min(100, Math.max(0, score))}%`;
+ const gradient = score >= 70 ? 'linear-gradient(90deg, #10b981, #34d399)' : score >= 50 ? 'linear-gradient(90deg, #f59e0b, #fbbf24)' : 'linear-gradient(90deg, #ef4444, #f87171)';
+ railFill.style.background = gradient;
+ railFill.style.boxShadow = `0 0 8px ${glow}`;
+ }
+ }
+ };
+
+ const doTruthVote = async (vote) => {
+ if (!isAuthed()) {
+ requestAuth("Please sign in or create a free account to vote on truth.");
+ return;
+ }
+ if (isVoting) return;
+ isVoting = true;
+
+ // Visual feedback
+ if (vote === 'true' && upvoteBtn) {
+ upvoteBtn.style.transform = 'scale(1.15)';
+ upvoteBtn.style.background = 'rgba(52, 211, 153, 0.4)';
+ } else if (vote === 'false' && downvoteBtn) {
+ downvoteBtn.style.transform = 'scale(1.15)';
+ downvoteBtn.style.background = 'rgba(239, 68, 68, 0.4)';
+ }
+
+ try {
+ trackEvent("truth_vote_click", { post_id: postID, vote, context: "map" });
+ const stats = await votePostTruth(postID, vote);
+ if (stats) {
+ currentVote = vote;
+ if (stats.truth_score !== null && Number.isFinite(stats.truth_score)) {
+ updateTruthUI(stats.truth_score);
+ }
+ }
+ } catch (err) {
+ console.error('Truth vote error:', err);
+ }
+
+ // Reset button visuals
+ setTimeout(() => {
+ if (upvoteBtn) {
+ upvoteBtn.style.transform = '';
+ upvoteBtn.style.background = currentVote === 'true' ? 'rgba(52, 211, 153, 0.35)' : 'rgba(52, 211, 153, 0.15)';
+ }
+ if (downvoteBtn) {
+ downvoteBtn.style.transform = '';
+ downvoteBtn.style.background = currentVote === 'false' ? 'rgba(239, 68, 68, 0.35)' : 'rgba(239, 68, 68, 0.15)';
+ }
+ isVoting = false;
+ }, 200);
+ };
+
+ if (downvoteBtn) {
+ downvoteBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ doTruthVote('false');
+ });
+ }
+ if (upvoteBtn) {
+ upvoteBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ doTruthVote('true');
+ });
+ }
+ }
+
// Author action handlers (edit, visibility, delete)
if (isAuthor && postID > 0) {
const editBtn = modalWrap.querySelector('[data-action="edit-post"]');
diff --git a/src/components/Posts/PostCardNew.jsx b/src/components/Posts/PostCardNew.jsx
index 6d12bcf..9df418d 100644
--- a/src/components/Posts/PostCardNew.jsx
+++ b/src/components/Posts/PostCardNew.jsx
@@ -313,7 +313,7 @@ export default function PostCardNew({ post, selected, onSelect, onPostDeleted, o
setPostGroupId((post?.group_id || "").toString());
}, [post?.visibility, post?.group_id]);
- // Media URLs
+ // Media URLs - check both media_urls (frontend) and image_urls (backend API)
const mediaUrls = useMemo(() => {
const list = [];
const seen = new Set();
@@ -323,12 +323,18 @@ export default function PostCardNew({ post, selected, onSelect, onPostDeleted, o
seen.add(normalized);
list.push(normalized);
};
+ // Check media_urls (frontend sends this)
if (Array.isArray(post?.media_urls)) {
post.media_urls.forEach(addUrl);
}
+ // Check image_urls (backend API returns this)
+ if (Array.isArray(post?.image_urls)) {
+ post.image_urls.forEach(addUrl);
+ }
+ // Fallback to individual image fields (already deduped by seen Set)
[post?.image_large, post?.image_medium, post?.image_small, post?.image, post?.media_url].forEach(addUrl);
return list;
- }, [post?.media_urls, post?.image_large, post?.image_medium, post?.image_small, post?.image, post?.media_url]);
+ }, [post?.media_urls, post?.image_urls, post?.image_large, post?.image_medium, post?.image_small, post?.image, post?.media_url]);
const mediaKey = mediaUrls.join("|");
diff --git a/src/components/Posts/PostCreateModal.jsx b/src/components/Posts/PostCreateModal.jsx
index 1de54d0..e7c848a 100644
--- a/src/components/Posts/PostCreateModal.jsx
+++ b/src/components/Posts/PostCreateModal.jsx
@@ -51,18 +51,22 @@ export default function PostCreateModal({
const textareaRef = useRef(null);
const linkTimeoutRef = useRef(null);
- // Reset on open
+ // Reset only when modal opens (not on every initialCategory change)
+ const wasOpenRef = useRef(false);
useEffect(() => {
- if (isOpen) {
+ if (isOpen && !wasOpenRef.current) {
+ // Modal just opened - reset everything
setMode("text");
setTitle("");
setBody("");
setLinkUrl("");
setLinkPreview(null);
setImages([]);
+ setUploadError("");
setCategory(initialCategory);
setSubCategory(SUB_CATEGORIES[initialCategory]?.[0] || "");
}
+ wasOpenRef.current = isOpen;
}, [isOpen, initialCategory]);
// Auto-resize textarea
@@ -88,28 +92,37 @@ export default function PostCreateModal({
if (preview) {
setLinkPreview(preview);
if (!title && preview.title) setTitle(preview.title);
+ if (!body && preview.description) setBody(preview.description);
}
} catch {}
setLinkLoading(false);
}, 500);
return () => clearTimeout(linkTimeoutRef.current);
- }, [linkUrl, mode, title]);
+ }, [linkUrl, mode, title, body]);
+
+ const [uploadError, setUploadError] = useState("");
const handleFileChange = useCallback(async (e) => {
const files = Array.from(e.target.files || []);
if (files.length === 0) return;
setUploading(true);
+ setUploadError("");
try {
for (const file of files) {
const result = await uploadPostMedia(file);
if (result?.url) {
- setImages((prev) => [...prev, result.url]);
+ setImages((prev) => {
+ const newImages = [...prev, result.url];
+ return newImages;
+ });
+ } else {
+ setUploadError(result?.error || "Upload failed - no URL returned");
}
}
} catch (err) {
- console.error("Upload failed:", err);
+ setUploadError(err.message || "Upload failed");
}
setUploading(false);
e.target.value = "";
@@ -148,23 +161,28 @@ export default function PostCreateModal({
return (
- {/* Backdrop */}
+ {/* Backdrop - only covers bottom half, allows map interaction above */}
+ {/* Invisible close trigger at top */}
+
- {/* Modal */}
+ {/* Modal - positioned at bottom, shorter to show map with crosshair */}
{/* Scrollable content */}
-
+
{/* Category row */}
{CATEGORIES.map((cat) => (
@@ -390,7 +408,15 @@ export default function PostCreateModal({
)}
- {/* Error */}
+ {/* Upload Error */}
+ {uploadError && (
+
+
+ {uploadError}
+
+ )}
+
+ {/* Save Error */}
{saveError && (
@@ -399,10 +425,15 @@ export default function PostCreateModal({
)}
{/* Location */}
- {coords && (
+ {coords ? (
- Location pinned on map
+ Location: {coords[1]?.toFixed(4)}, {coords[0]?.toFixed(4)}
+
+ ) : (
+
+
+ No location - post may fail
)}
diff --git a/src/components/Posts/PostDetailModal.jsx b/src/components/Posts/PostDetailModal.jsx
index cfa692a..75a7aa5 100644
--- a/src/components/Posts/PostDetailModal.jsx
+++ b/src/components/Posts/PostDetailModal.jsx
@@ -140,14 +140,35 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
const postId = post?.id || post?._id;
const isAuthor = username && (post?.author === username || post?.username === username);
- // Media gallery
+ // Media gallery - deduplicated from all possible image fields
const gallery = useMemo(() => {
+ const seen = new Set();
const images = [];
- if (post?.image_url) images.push(normalizeMediaUrl(post.image_url));
+ const addUrl = (url) => {
+ const normalized = normalizeMediaUrl(url);
+ if (!normalized || seen.has(normalized)) return;
+ seen.add(normalized);
+ images.push(normalized);
+ };
+
+ // Check media_urls first (frontend sends this)
+ if (Array.isArray(post?.media_urls)) {
+ post.media_urls.forEach(addUrl);
+ }
+ // Check image_urls (backend API returns this)
+ if (Array.isArray(post?.image_urls)) {
+ post.image_urls.forEach(addUrl);
+ }
+ // Check individual image fields (fallback, often same URL)
+ addUrl(post?.image_large);
+ addUrl(post?.image_medium);
+ addUrl(post?.image_small);
+ addUrl(post?.image_url);
+ addUrl(post?.image);
+ // Check media array if present
if (Array.isArray(post?.media)) {
post.media.forEach((m) => {
- const url = normalizeMediaUrl(m?.url || m?.thumbnail_url || "");
- if (url && !images.includes(url)) images.push(url);
+ addUrl(m?.url || m?.thumbnail_url);
});
}
return images;
@@ -186,10 +207,12 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
// Fetch truth scores
fetchPostTruth(postId).then((res) => {
if (res) {
+ // Use truth_score as the main display value (0-100 scale)
+ const mainScore = res.truth_score ?? res.user_score ?? 50;
setTruth({
ai: res.ai_score ?? 50,
- user: res.user_score ?? 50,
- count: res.vote_count ?? 0,
+ user: mainScore,
+ count: res.vote_count ?? res.total_votes ?? 0,
});
}
});
@@ -257,10 +280,12 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
if (!postId) return;
const res = await votePostTruth(postId, vote);
if (res) {
+ // Use truth_score as the main display value (0-100 scale)
+ const mainScore = res.truth_score ?? res.user_score ?? truth.user;
setTruth({
ai: res.ai_score ?? truth.ai,
- user: res.user_score ?? truth.user,
- count: res.vote_count ?? truth.count,
+ user: mainScore,
+ count: res.vote_count ?? res.total_votes ?? truth.count,
});
}
}, [postId, truth]);
@@ -551,41 +576,82 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
))}
- {/* Truth score */}
-
-
-
- Truth Score
-
- {truth.count} votes
-
-
-
-
-
+ {/* Truth Score - Social Network Style */}
+
+
+ {/* Downvote button */}
handleTruthVote("up")}
+ className="w-12 h-12 rounded-full flex items-center justify-center
+ bg-gradient-to-br from-red-500/20 to-orange-500/10
+ border border-red-500/30 text-red-400
+ hover:from-red-500/30 hover:to-orange-500/20 hover:border-red-400/50
+ active:scale-95 transition-all shadow-lg shadow-red-500/10"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.9 }}
+ onClick={() => handleTruthVote("false")}
>
-
- True
+
-
{Math.round(truth.user)}%
+
+ {/* Score display with rail */}
+
+
+ = 70 ? 'text-emerald-400' :
+ truth.user >= 50 ? 'text-amber-400' :
+ 'text-red-400'
+ }`}>
+ {Math.round(truth.user)}
+
+ / 100
+
+ {/* Progress rail */}
+
+ = 70
+ ? 'bg-gradient-to-r from-emerald-500 via-green-400 to-teal-400'
+ : truth.user >= 50
+ ? 'bg-gradient-to-r from-amber-500 via-yellow-400 to-orange-400'
+ : 'bg-gradient-to-r from-red-500 via-rose-400 to-pink-400'
+ }`}
+ initial={{ width: 0 }}
+ animate={{ width: `${truth.user}%` }}
+ transition={{ duration: 0.6, ease: "easeOut" }}
+ />
+ {/* Glow effect */}
+ = 70
+ ? 'bg-gradient-to-r from-emerald-400 to-teal-400'
+ : truth.user >= 50
+ ? 'bg-gradient-to-r from-amber-400 to-orange-400'
+ : 'bg-gradient-to-r from-red-400 to-pink-400'
+ }`}
+ initial={{ width: 0 }}
+ animate={{ width: `${truth.user}%` }}
+ transition={{ duration: 0.6, ease: "easeOut" }}
+ />
+
+
+ Questionable
+ {truth.count} votes
+ Verified
+
+
+
+ {/* Upvote button */}
handleTruthVote("down")}
+ className="w-12 h-12 rounded-full flex items-center justify-center
+ bg-gradient-to-br from-emerald-500/20 to-teal-500/10
+ border border-emerald-500/30 text-emerald-400
+ hover:from-emerald-500/30 hover:to-teal-500/20 hover:border-emerald-400/50
+ active:scale-95 transition-all shadow-lg shadow-emerald-500/10"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.9 }}
+ onClick={() => handleTruthVote("true")}
>
-
- False
+
diff --git a/src/styles/map.css b/src/styles/map.css
index 8b38ce5..ab16f09 100644
--- a/src/styles/map.css
+++ b/src/styles/map.css
@@ -210,10 +210,10 @@
}
-/* Crosshair pour création */
+/* Crosshair pour création - centered for mobile thumb reach */
.map-crosshair{
left:50%;
- top:30%;
+ top:45%;
transform:translate(-50%,-50%);
}
.crosshair-aim{
@@ -1913,9 +1913,9 @@ body[data-theme="blue"] .mode-selector-v2{
/* Location Picker UI */
.location-picker-ui {
position: absolute;
- bottom: 24px;
+ top: 58%; /* Position hint below crosshair (which is at 45%) */
left: 50%;
- transform: translateX(-50%);
+ transform: translate(-50%, 0);
z-index: 1100;
display: flex;
flex-direction: column;