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:
Your Name 2026-01-24 23:15:56 +00:00
parent 717e5f64a1
commit fa90d7426b
4 changed files with 80 additions and 43 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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
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")); window.dispatchEvent(new CustomEvent("sw:openMapPostCreator"));
}
}; };
return ( return (

View File

@ -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>
)} )}