v115: Use existing FAB for island posts with island_username context
- FAB (+) button now context-aware - includes island_username when on island - PostComposer accepts islandUsername prop and includes in payload - Removed custom post buttons from island terrain view - Global __activeIslandContext set when viewing an island - App.jsx handles sw:openPostCreator event for island posts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
717e5f64a1
commit
fa90d7426b
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "sociowire-frontend",
|
"name": "sociowire-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.114",
|
"version": "0.0.115",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
|
|
||||||
46
src/App.jsx
46
src/App.jsx
|
|
@ -7,7 +7,7 @@ import ContentFilters from "./components/Filters/ContentFilters";
|
||||||
import IslandViewer from "./components/Island/IslandViewer";
|
import IslandViewer from "./components/Island/IslandViewer";
|
||||||
import ChatContainer from "./components/Chat/ChatContainer";
|
import ChatContainer from "./components/Chat/ChatContainer";
|
||||||
import PostDetailModal from "./components/Posts/PostDetailModal";
|
import PostDetailModal from "./components/Posts/PostDetailModal";
|
||||||
import { CreatePostFAB } from "./components/Posts/PostComposer";
|
import PostComposer, { CreatePostFAB } from "./components/Posts/PostComposer";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { fetchIslandByUsername } from "./api/client";
|
import { fetchIslandByUsername } from "./api/client";
|
||||||
|
|
||||||
|
|
@ -66,6 +66,10 @@ export default function App() {
|
||||||
// Island Viewer state (for viewing individual islands)
|
// Island Viewer state (for viewing individual islands)
|
||||||
const [selectedIsland, setSelectedIsland] = useState(null);
|
const [selectedIsland, setSelectedIsland] = useState(null);
|
||||||
|
|
||||||
|
// Post Composer state (for creating posts with island context)
|
||||||
|
const [showPostComposer, setShowPostComposer] = useState(false);
|
||||||
|
const [postComposerIsland, setPostComposerIsland] = useState(null);
|
||||||
|
|
||||||
// Post detail modal state (triggered from map markers)
|
// Post detail modal state (triggered from map markers)
|
||||||
const [detailPost, setDetailPost] = useState(null);
|
const [detailPost, setDetailPost] = useState(null);
|
||||||
|
|
||||||
|
|
@ -152,16 +156,30 @@ export default function App() {
|
||||||
return () => window.removeEventListener("sw:openPostDetail", handleOpenPostDetail);
|
return () => window.removeEventListener("sw:openPostDetail", handleOpenPostDetail);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Listen for island post creation event
|
// Listen for post creator events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Handle opening post composer with island context
|
||||||
|
const handleOpenPostCreator = (e) => {
|
||||||
|
const { island_username } = e.detail || {};
|
||||||
|
setPostComposerIsland(island_username || null);
|
||||||
|
setShowPostComposer(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Legacy island post event
|
||||||
const handleIslandPost = (e) => {
|
const handleIslandPost = (e) => {
|
||||||
const { island } = e.detail || {};
|
const { island } = e.detail || {};
|
||||||
if (island) {
|
if (island) {
|
||||||
window.location.href = `/island/${island}?create=true`;
|
setPostComposerIsland(island);
|
||||||
|
setShowPostComposer(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.addEventListener('sw:openPostCreator', handleOpenPostCreator);
|
||||||
window.addEventListener('sociowire:create-post', handleIslandPost);
|
window.addEventListener('sociowire:create-post', handleIslandPost);
|
||||||
return () => window.removeEventListener('sociowire:create-post', handleIslandPost);
|
return () => {
|
||||||
|
window.removeEventListener('sw:openPostCreator', handleOpenPostCreator);
|
||||||
|
window.removeEventListener('sociowire:create-post', handleIslandPost);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// World navigation handlers
|
// World navigation handlers
|
||||||
|
|
@ -362,6 +380,26 @@ export default function App() {
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Post Composer Modal (for island posts) */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showPostComposer && (
|
||||||
|
<PostComposer
|
||||||
|
key="post-composer"
|
||||||
|
islandUsername={postComposerIsland}
|
||||||
|
onClose={() => {
|
||||||
|
setShowPostComposer(false);
|
||||||
|
setPostComposerIsland(null);
|
||||||
|
}}
|
||||||
|
onPostCreated={(newPost) => {
|
||||||
|
// Refresh wall posts if on map view
|
||||||
|
if (currentView === 'map') {
|
||||||
|
setWallPosts((prev) => [newPost, ...prev]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Floating Action Button for creating posts */}
|
{/* Floating Action Button for creating posts */}
|
||||||
<CreatePostFAB />
|
<CreatePostFAB />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const CATEGORIES = [
|
||||||
{ id: "weather", icon: "fa-cloud-sun", label: "Weather", color: "from-sky-500 to-blue-500" },
|
{ id: "weather", icon: "fa-cloud-sun", label: "Weather", color: "from-sky-500 to-blue-500" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function PostComposer({ onClose, onPostCreated }) {
|
export default function PostComposer({ onClose, onPostCreated, islandUsername }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { token, username, isAuthenticated } = useAuth();
|
const { token, username, isAuthenticated } = useAuth();
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
@ -159,6 +159,11 @@ export default function PostComposer({ onClose, onPostCreated }) {
|
||||||
payload.location_name = locationName || "";
|
payload.location_name = locationName || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add island context if posting to an island
|
||||||
|
if (islandUsername) {
|
||||||
|
payload.island_username = islandUsername;
|
||||||
|
}
|
||||||
|
|
||||||
// Add media URLs
|
// Add media URLs
|
||||||
if (uploadedMedia.length > 0) {
|
if (uploadedMedia.length > 0) {
|
||||||
const firstMedia = uploadedMedia[0];
|
const firstMedia = uploadedMedia[0];
|
||||||
|
|
@ -493,10 +498,20 @@ export default function PostComposer({ onClose, onPostCreated }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Floating Action Button for creating posts - triggers the map's create post flow
|
// Floating Action Button for creating posts - triggers the map's create post flow
|
||||||
|
// Context-aware: if viewing an island, includes island info
|
||||||
export function CreatePostFAB() {
|
export function CreatePostFAB() {
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
// Dispatch event to open the map's post creation modal
|
// Check if we're viewing an island
|
||||||
window.dispatchEvent(new CustomEvent("sw:openMapPostCreator"));
|
const islandContext = window.__activeIslandContext;
|
||||||
|
if (islandContext && islandContext.isOwner) {
|
||||||
|
// Open post creator with island context
|
||||||
|
window.dispatchEvent(new CustomEvent("sw:openPostCreator", {
|
||||||
|
detail: { island_username: islandContext.username }
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// Default: open map's post creation modal
|
||||||
|
window.dispatchEvent(new CustomEvent("sw:openMapPostCreator"));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,21 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
const [islandPosts, setIslandPosts] = useState([]);
|
const [islandPosts, setIslandPosts] = useState([]);
|
||||||
const [loadingPosts, setLoadingPosts] = useState(false);
|
const [loadingPosts, setLoadingPosts] = useState(false);
|
||||||
|
|
||||||
|
// Set global island context when zoomed (for FAB to use)
|
||||||
|
useEffect(() => {
|
||||||
|
if (zoomedIsland) {
|
||||||
|
window.__activeIslandContext = {
|
||||||
|
username: zoomedIsland.username,
|
||||||
|
isOwner: username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
window.__activeIslandContext = null;
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
window.__activeIslandContext = null;
|
||||||
|
};
|
||||||
|
}, [zoomedIsland, username]);
|
||||||
|
|
||||||
// Fetch islands
|
// Fetch islands
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
@ -289,19 +304,9 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
<i className="fa-solid fa-user" />
|
<i className="fa-solid fa-user" />
|
||||||
</button>
|
</button>
|
||||||
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
|
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
|
||||||
<>
|
<button className="island-terrain-btn" onClick={() => window.location.href = `/island/${zoomedIsland.username}/edit`}>
|
||||||
<button className="island-terrain-btn" onClick={() => window.location.href = `/island/${zoomedIsland.username}/edit`}>
|
<i className="fa-solid fa-cog" />
|
||||||
<i className="fa-solid fa-cog" />
|
</button>
|
||||||
</button>
|
|
||||||
<button className="island-terrain-btn island-terrain-post-btn" onClick={() => {
|
|
||||||
window.dispatchEvent(new CustomEvent('sociowire:create-post', {
|
|
||||||
detail: { island: zoomedIsland.username }
|
|
||||||
}));
|
|
||||||
}}>
|
|
||||||
<i className="fa-solid fa-plus" />
|
|
||||||
<span>{t('worlds.postHere')}</span>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -318,14 +323,7 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
<i className="fa-solid fa-umbrella-beach" />
|
<i className="fa-solid fa-umbrella-beach" />
|
||||||
<p>{t('worlds.emptyIsland')}</p>
|
<p>{t('worlds.emptyIsland')}</p>
|
||||||
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
|
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
|
||||||
<button className="island-terrain-first-post" onClick={() => {
|
<p className="island-terrain-hint">{t('worlds.useButtonToPost')}</p>
|
||||||
window.dispatchEvent(new CustomEvent('sociowire:create-post', {
|
|
||||||
detail: { island: zoomedIsland.username }
|
|
||||||
}));
|
|
||||||
}}>
|
|
||||||
<i className="fa-solid fa-plus" />
|
|
||||||
{t('worlds.createFirstPost')}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -334,20 +332,6 @@ export default function IslandsWorld({ theme, onIslandClick, onOpenMap, onOpenPl
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating create post button for island owner */}
|
|
||||||
{username && username.toLowerCase() === (zoomedIsland.username || '').toLowerCase() && (
|
|
||||||
<button
|
|
||||||
className="island-create-post-fab"
|
|
||||||
onClick={() => {
|
|
||||||
window.dispatchEvent(new CustomEvent('sociowire:create-post', {
|
|
||||||
detail: { island: zoomedIsland.username }
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="fa-solid fa-plus" />
|
|
||||||
<span>{t('worlds.newPost')}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue