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",
|
"name": "sociowire-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.50",
|
"version": "0.0.51",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import "../../styles/filters.css";
|
||||||
import { createPost, fetchPostPreview, resolveAssetRef } from "../../api/client";
|
import { createPost, fetchPostPreview, resolveAssetRef } from "../../api/client";
|
||||||
import { LiveKitBroadcast, LiveKitViewerModal } from "../Live";
|
import { LiveKitBroadcast, LiveKitViewerModal } from "../Live";
|
||||||
import { WeatherModal } from "../Weather";
|
import { WeatherModal } from "../Weather";
|
||||||
|
import PostCreateModal from "../Posts/PostCreateModal";
|
||||||
import {
|
import {
|
||||||
FILTER_MAIN_CATEGORIES,
|
FILTER_MAIN_CATEGORIES,
|
||||||
FILTER_CATEGORY_MAP,
|
FILTER_CATEGORY_MAP,
|
||||||
|
|
@ -1530,42 +1531,15 @@ export default function MapView({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsCreating(true);
|
// Get current map center as default coords
|
||||||
setCreateStep(0);
|
const map = mapRef.current;
|
||||||
setDraftTitle("");
|
if (map) {
|
||||||
setDraftBody("");
|
const c = map.getCenter();
|
||||||
setDraftUrl("");
|
setDraftCoords([c.lng, c.lat]);
|
||||||
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("");
|
|
||||||
setSaveError("");
|
setSaveError("");
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
setTitleTouched(false);
|
setIsCreating(true);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update ref for long-press callback
|
// Update ref for long-press callback
|
||||||
|
|
@ -2159,227 +2133,62 @@ export default function MapView({
|
||||||
weatherEnabled={weatherViewEnabled}
|
weatherEnabled={weatherViewEnabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isCreating && modeChosen && (
|
{/* New Post Creation Modal */}
|
||||||
<div className="map-overlay map-crosshair">
|
<PostCreateModal
|
||||||
<div className="crosshair-aim" />
|
isOpen={isCreating}
|
||||||
</div>
|
onClose={closeCreateModal}
|
||||||
)}
|
onSubmit={async (data) => {
|
||||||
|
setIsSaving(true);
|
||||||
{modeSelectorVisible && (
|
setSaveError("");
|
||||||
<>
|
try {
|
||||||
<div className="mode-selector-backdrop" onClick={closeCreateModal} />
|
const map = mapRef.current;
|
||||||
<div className="map-overlay mode-selector-widget mode-selector-v2">
|
let coords = data.coords;
|
||||||
<button className="mode-selector-close" onClick={closeCreateModal} title="Close">
|
if (!coords && map) {
|
||||||
<i className="fa-solid fa-xmark" />
|
const c = map.getCenter();
|
||||||
</button>
|
coords = [c.lng, c.lat];
|
||||||
<div className="mode-selector-title">Create a post</div>
|
}
|
||||||
<div className="mode-selector-chips">
|
const payload = {
|
||||||
{CONTENT_CREATOR_MODES.map((mode) => (
|
title: data.title,
|
||||||
<button
|
body: data.body,
|
||||||
key={mode.id}
|
category: data.category === "Events" ? "EVENT" : data.category.toUpperCase(),
|
||||||
className={
|
sub_category: data.subCategory,
|
||||||
"mode-selector-chip" +
|
content_type: data.mode === "photo" ? "photo" : (data.mode === "link" ? "link" : "text"),
|
||||||
(mode.id === contentMode ? " is-active" : "")
|
longitude: coords?.[0] ?? 0,
|
||||||
}
|
latitude: coords?.[1] ?? 0,
|
||||||
onClick={() => handleModeSelect(mode.id)}
|
url: data.linkUrl || "",
|
||||||
>
|
image_small: data.images?.[0] || data.linkPreview?.image || "",
|
||||||
<i className={mode.icon} />
|
image_medium: data.images?.[1] || data.images?.[0] || data.linkPreview?.image || "",
|
||||||
<span>{mode.label}</span>
|
image_large: data.images?.[2] || data.images?.[1] || data.images?.[0] || data.linkPreview?.image || "",
|
||||||
</button>
|
media_urls: data.images || [],
|
||||||
))}
|
source: data.linkPreview?.site_name || "",
|
||||||
</div>
|
};
|
||||||
</div>
|
const result = await createPost(payload);
|
||||||
</>
|
if (result?.id) {
|
||||||
)}
|
setIsCreating(false);
|
||||||
|
// Refresh posts
|
||||||
{isCreating && modeChosen && (
|
if (typeof refreshPosts === "function") refreshPosts();
|
||||||
<div className="create-floating-ui">
|
}
|
||||||
{/* Close button */}
|
} catch (err) {
|
||||||
<button className="cfloat-close" onClick={closeCreateModal}>
|
setSaveError(err.message || "Failed to create post");
|
||||||
<i className="fa-solid fa-xmark" />
|
}
|
||||||
</button>
|
setIsSaving(false);
|
||||||
|
}}
|
||||||
{/* Step indicator */}
|
onGoLive={(data) => {
|
||||||
<div className="cfloat-box cfloat-step">
|
const map = mapRef.current;
|
||||||
<i className={currentContentMode.icon} />
|
let coords = data.coords;
|
||||||
<span>Step {createStep + 1}/3</span>
|
if (!coords && map) {
|
||||||
</div>
|
const c = map.getCenter();
|
||||||
|
coords = [c.lng, c.lat];
|
||||||
{/* STEP 0 - Position hint */}
|
}
|
||||||
{createStep === 0 && (
|
setLiveCoords(coords);
|
||||||
<div className="cfloat-box cfloat-hint">
|
setShowLiveBroadcast(true);
|
||||||
<p>{currentFlow.hint}</p>
|
setIsCreating(false);
|
||||||
</div>
|
}}
|
||||||
)}
|
coords={draftCoords}
|
||||||
|
initialCategory={mainFilter === "All" ? "News" : mainFilter}
|
||||||
{/* STEP 1 - Category */}
|
isSaving={isSaving}
|
||||||
{createStep === 1 && (
|
saveError={saveError}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { uploadImage, fetchLinkPreview } from "../../api/client";
|
import { uploadPostMedia, fetchPostPreview } from "../../api/client";
|
||||||
|
|
||||||
// Content modes
|
// Content modes
|
||||||
const CONTENT_MODES = [
|
const CONTENT_MODES = [
|
||||||
|
|
@ -84,7 +84,7 @@ export default function PostCreateModal({
|
||||||
linkTimeoutRef.current = setTimeout(async () => {
|
linkTimeoutRef.current = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
setLinkLoading(true);
|
setLinkLoading(true);
|
||||||
const preview = await fetchLinkPreview(linkUrl.trim());
|
const preview = await fetchPostPreview(linkUrl.trim());
|
||||||
if (preview) {
|
if (preview) {
|
||||||
setLinkPreview(preview);
|
setLinkPreview(preview);
|
||||||
if (!title && preview.title) setTitle(preview.title);
|
if (!title && preview.title) setTitle(preview.title);
|
||||||
|
|
@ -103,7 +103,7 @@ export default function PostCreateModal({
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
try {
|
try {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const result = await uploadImage(file);
|
const result = await uploadPostMedia(file);
|
||||||
if (result?.url) {
|
if (result?.url) {
|
||||||
setImages((prev) => [...prev, result.url]);
|
setImages((prev) => [...prev, result.url]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue