feat: Integrate new PostCreateModal into MapView

- Replace old 3-step wizard with single-screen PostCreateModal
- Simplify handleOpenCreate function
- Connect modal submit to createPost API
- Connect Go Live button to LiveKit broadcast

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-01-19 17:17:09 +00:00
parent 77e376465d
commit 9cdcc28784
3 changed files with 68 additions and 259 deletions

View File

@ -1,7 +1,7 @@
{
"name": "sociowire-frontend",
"private": true,
"version": "0.0.50",
"version": "0.0.51",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",

View File

@ -8,6 +8,7 @@ import "../../styles/filters.css";
import { createPost, fetchPostPreview, resolveAssetRef } from "../../api/client";
import { LiveKitBroadcast, LiveKitViewerModal } from "../Live";
import { WeatherModal } from "../Weather";
import PostCreateModal from "../Posts/PostCreateModal";
import {
FILTER_MAIN_CATEGORIES,
FILTER_CATEGORY_MAP,
@ -1530,42 +1531,15 @@ export default function MapView({
return;
}
setIsCreating(true);
setCreateStep(0);
setDraftTitle("");
setDraftBody("");
setDraftUrl("");
setDraftSource("");
setDraftPublishedAt("");
setDraftImage("");
setDraftImageSmall("");
setDraftImageMedium("");
setDraftImageLarge("");
setDraftImageFile(null);
setDraftImagePreview(LINK_PREVIEW_PLACEHOLDER);
setDraftMediaUrls([]);
setUseCustomImage(false);
setPostVisibility("public");
setPostGroupId("");
setPostUsers("");
setImageVisibility("public");
setImageGroupId("");
setImageUsers("");
setUrlPreviewLoading(false);
setUrlPreviewError("");
// Get current map center as default coords
const map = mapRef.current;
if (map) {
const c = map.getCenter();
setDraftCoords([c.lng, c.lat]);
}
setSaveError("");
setIsSaving(false);
setTitleTouched(false);
setBodyTouched(false);
setImageTouched(false);
lastPreviewUrlRef.current = "";
const main = mainFilter === "All" ? "News" : mainFilter;
setDraftCategory(main);
const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News;
setDraftSubCategory(list[0]);
setDraftCoords(null);
setModeSelectorVisible(true);
setModeChosen(false);
setIsCreating(true);
};
// Update ref for long-press callback
@ -2159,227 +2133,62 @@ export default function MapView({
weatherEnabled={weatherViewEnabled}
/>
{isCreating && modeChosen && (
<div className="map-overlay map-crosshair">
<div className="crosshair-aim" />
</div>
)}
{modeSelectorVisible && (
<>
<div className="mode-selector-backdrop" onClick={closeCreateModal} />
<div className="map-overlay mode-selector-widget mode-selector-v2">
<button className="mode-selector-close" onClick={closeCreateModal} title="Close">
<i className="fa-solid fa-xmark" />
</button>
<div className="mode-selector-title">Create a post</div>
<div className="mode-selector-chips">
{CONTENT_CREATOR_MODES.map((mode) => (
<button
key={mode.id}
className={
"mode-selector-chip" +
(mode.id === contentMode ? " is-active" : "")
}
onClick={() => handleModeSelect(mode.id)}
>
<i className={mode.icon} />
<span>{mode.label}</span>
</button>
))}
</div>
</div>
</>
)}
{isCreating && modeChosen && (
<div className="create-floating-ui">
{/* Close button */}
<button className="cfloat-close" onClick={closeCreateModal}>
<i className="fa-solid fa-xmark" />
</button>
{/* Step indicator */}
<div className="cfloat-box cfloat-step">
<i className={currentContentMode.icon} />
<span>Step {createStep + 1}/3</span>
</div>
{/* STEP 0 - Position hint */}
{createStep === 0 && (
<div className="cfloat-box cfloat-hint">
<p>{currentFlow.hint}</p>
</div>
)}
{/* STEP 1 - Category */}
{createStep === 1 && (
<div className="cfloat-box cfloat-category">
<label>Category</label>
<div className="cfloat-selects">
<select value={draftCategory} onChange={(e) => {
setDraftCategory(e.target.value);
setDraftSubCategory((CREATE_CATEGORY_MAP[e.target.value] || CREATE_CATEGORY_MAP.News)[0]);
}}>
{['News', 'Friends', 'Events', 'Market'].map((c) => <option key={c}>{c}</option>)}
</select>
<select value={draftSubCategory} onChange={(e) => setDraftSubCategory(e.target.value)}>
{(CREATE_CATEGORY_MAP[draftCategory] || CREATE_CATEGORY_MAP.News).map((s) => <option key={s}>{s}</option>)}
</select>
</div>
</div>
)}
{/* STEP 1 - Title */}
{createStep === 1 && (
<div className="cfloat-box cfloat-title">
<label>Title or Link</label>
<input
placeholder={currentFlow.placeholder}
value={draftTitle}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={(e) => {
const v = e.target.value;
if (isLikelyUrl(v)) {
setDraftTitle(v); setTitleTouched(false); setDraftUrl(v.trim());
setUrlPreviewError(""); setDraftSource(""); setDraftPublishedAt("");
handleUrlPreview(v);
} else {
setDraftTitle(v); setTitleTouched(true);
}
}}
onPaste={(e) => {
const pasted = e.clipboardData?.getData('text') || '';
if (pasted && isLikelyUrl(pasted.trim())) {
e.preventDefault();
const v = pasted.trim();
setDraftTitle(v); setTitleTouched(false); setDraftUrl(v);
setUrlPreviewError(""); setDraftSource(""); setDraftPublishedAt("");
handleUrlPreview(v);
}
}}
/>
{draftUrl && !draftSource && <span className="cfloat-meta">Fetching preview...</span>}
{draftSource && <span className="cfloat-meta">{draftSource}</span>}
{urlPreviewError && <span className="cfloat-error">{urlPreviewError}</span>}
</div>
)}
{/* STEP 2 - Preview */}
{createStep === 2 && shouldShowPreviewCard && (
<div className="cfloat-box cfloat-preview">
<img src={galleryPreviewImage || LINK_PREVIEW_PLACEHOLDER} alt="Preview" />
{draftSource && <span>{draftSource}</span>}
</div>
)}
{/* STEP 2 - Description */}
{createStep === 2 && (
<div className="cfloat-box cfloat-desc">
<label>Description</label>
<textarea
rows={3}
placeholder={currentFlow.bodyPlaceholder}
value={draftBody}
onChange={(e) => { setDraftBody(e.target.value); setBodyTouched(true); }}
/>
</div>
)}
{/* STEP 2 - Image */}
{createStep === 2 && (
<div className="cfloat-box cfloat-image">
<div className="cfloat-img-toggle">
<button className={!useCustomImage ? "active" : ""} onClick={() => setUseCustomImage(false)}>
<i className="fa-solid fa-icons" /> Icon
</button>
<button className={useCustomImage ? "active" : ""} onClick={() => setUseCustomImage(true)}>
<i className="fa-solid fa-image" /> Image
</button>
</div>
{useCustomImage && (
<>
<div className="cfloat-img-upload">
<label>
<i className="fa-solid fa-upload" /> Upload
<input type="file" accept="image/*" multiple onChange={handleImageFileChange} disabled={uploadingImage} hidden />
</label>
<label>
<i className="fa-solid fa-camera" /> Camera
<input type="file" accept="image/*" capture="environment" onChange={handleImageFileChange} disabled={uploadingImage} hidden />
</label>
{uploadingImage && <span>Uploading...</span>}
</div>
{galleryItems.length > 0 && (
<div className="cfloat-gallery">
{galleryItems.map((url, idx) => (
<div key={idx} className="cfloat-gallery-item">
<img src={url} alt={`Upload ${idx + 1}`} />
<button onClick={() => removeMediaUrlAtIndex(idx)} title="Remove">
<i className="fa-solid fa-xmark" />
</button>
</div>
))}
</div>
)}
</>
)}
</div>
)}
{/* Error display */}
{saveError && (
<div className="cfloat-box cfloat-error-box">
<i className="fa-solid fa-exclamation-triangle" />
<span>{saveError}</span>
</div>
)}
{/* Navigation buttons */}
<div className="cfloat-box cfloat-nav">
<button className="cfloat-btn" onClick={handlePrevStep} disabled={createStep === 0}>
<i className="fa-solid fa-arrow-left" /> Back
</button>
<button
className="cfloat-btn cfloat-btn-primary"
disabled={isSaving}
onClick={() => {
if (contentMode === "live" && createStep === 0) {
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];
}
setLiveCoords(coords);
setShowLiveBroadcast(true);
setIsCreating(false);
setModeChosen(false);
}
return;
}
if (createStep < 2) { setSaveError(""); setCreateStep((p) => p + 1); }
else handleSubmitPost();
}}
>
{createStep === 2 ? (isSaving ? "Posting..." : "Post") : (contentMode === "live" && createStep === 0 ? "Go Live" : "Next")}
<i className="fa-solid fa-arrow-right" />
</button>
</div>
</div>
)}
{/* New Post Creation Modal */}
<PostCreateModal
isOpen={isCreating}
onClose={closeCreateModal}
onSubmit={async (data) => {
setIsSaving(true);
setSaveError("");
try {
const map = mapRef.current;
let coords = data.coords;
if (!coords && map) {
const c = map.getCenter();
coords = [c.lng, c.lat];
}
const payload = {
title: data.title,
body: data.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,
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 || "",
};
const result = await createPost(payload);
if (result?.id) {
setIsCreating(false);
// Refresh posts
if (typeof refreshPosts === "function") refreshPosts();
}
} catch (err) {
setSaveError(err.message || "Failed to create post");
}
setIsSaving(false);
}}
onGoLive={(data) => {
const map = mapRef.current;
let coords = data.coords;
if (!coords && map) {
const c = map.getCenter();
coords = [c.lng, c.lat];
}
setLiveCoords(coords);
setShowLiveBroadcast(true);
setIsCreating(false);
}}
coords={draftCoords}
initialCategory={mainFilter === "All" ? "News" : mainFilter}
isSaving={isSaving}
saveError={saveError}
/>
</div>

View File

@ -1,7 +1,7 @@
import React, { useState, useRef, useCallback, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useTranslation } from "react-i18next";
import { uploadImage, fetchLinkPreview } from "../../api/client";
import { uploadPostMedia, fetchPostPreview } from "../../api/client";
// Content modes
const CONTENT_MODES = [
@ -84,7 +84,7 @@ export default function PostCreateModal({
linkTimeoutRef.current = setTimeout(async () => {
try {
setLinkLoading(true);
const preview = await fetchLinkPreview(linkUrl.trim());
const preview = await fetchPostPreview(linkUrl.trim());
if (preview) {
setLinkPreview(preview);
if (!title && preview.title) setTitle(preview.title);
@ -103,7 +103,7 @@ export default function PostCreateModal({
setUploading(true);
try {
for (const file of files) {
const result = await uploadImage(file);
const result = await uploadPostMedia(file);
if (result?.url) {
setImages((prev) => [...prev, result.url]);
}