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:
parent
77e376465d
commit
9cdcc28784
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue