diff --git a/src/api/client.js b/src/api/client.js index 576b1ba..5817c22 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -229,6 +229,24 @@ export async function fetchPostById(postId) { } } +export async function updatePostVisibility(postId, payload = {}, token) { + if (!postId) throw new Error("post_id required"); + const headers = { "Content-Type": "application/json" }; + if (token) headers["Authorization"] = `Bearer ${token}`; + const body = JSON.stringify({ post_id: postId, ...payload }); + const res = await fetch(`${API_BASE}/post/visibility`, { + method: "PATCH", + headers, + credentials: "include", + body, + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || `HTTP ${res.status}`); + } + return res.json().catch(() => ({})); +} + /** diff --git a/src/components/Island/IslandViewer.jsx b/src/components/Island/IslandViewer.jsx index 5037097..aa2862f 100644 --- a/src/components/Island/IslandViewer.jsx +++ b/src/components/Island/IslandViewer.jsx @@ -1,7 +1,9 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import '../../styles/island.css'; +import { uploadProfileAvatar } from '../../api/client'; +import { useAuth } from '../../auth/AuthContext'; /** * IslandViewer: 2.5D immersive island visualization using MapLibre @@ -11,6 +13,12 @@ export default function IslandViewer({ island, onClose }) { const containerRef = useRef(null); const mapRef = useRef(null); const animationRef = useRef(null); + const fileRef = useRef(null); + const { authenticated, username, setAvatarUrl } = useAuth(); + const [uploading, setUploading] = useState(false); + const [avatarVisibility, setAvatarVisibility] = useState("public"); + const [avatarGroupId, setAvatarGroupId] = useState(""); + const [avatarUsers, setAvatarUsers] = useState(""); useEffect(() => { if (!island || !containerRef.current) return; @@ -131,6 +139,9 @@ export default function IslandViewer({ island, onClose }) { }, [island]); if (!island) return null; + const isSelf = + authenticated && username && island.username && + username.toLowerCase() === island.username.toLowerCase(); return (
@@ -142,6 +153,91 @@ export default function IslandViewer({ island, onClose }) {

{island.bio || 'Welcome to my island'}

+
+
+ {island.avatar_url ? ( + {island.username} + ) : ( +
+ {(island.username || "U")[0].toUpperCase()} +
+ )} +
+
+
@{island.username}
+ {isSelf ? ( +
+
+ + +
+ {avatarVisibility === "group" ? ( + setAvatarGroupId(e.target.value)} + /> + ) : null} + {avatarVisibility === "custom" ? ( + setAvatarUsers(e.target.value)} + /> + ) : null} + + { + const file = e.target.files && e.target.files[0]; + if (!file) return; + setUploading(true); + const users = avatarUsers + .split(",") + .map((u) => u.trim()) + .filter(Boolean); + const res = await uploadProfileAvatar(file, { + visibility: avatarVisibility, + group_id: avatarGroupId.trim(), + users, + }); + if (res && res.ok && res.avatar_url) { + island.avatar_url = res.avatar_url; + if (setAvatarUrl) setAvatarUrl(res.avatar_url); + } + setUploading(false); + }} + /> +
+ ) : null} +
+
+
u.trim()) + .filter(Boolean), }; if (useCustomImage && draftImage.trim()) { @@ -816,6 +828,40 @@ export default function MapView({ onChange={(e) => setDraftBody(e.target.value)} /> +
+ + +
+ {postVisibility === "group" ? ( + setPostGroupId(e.target.value)} + /> + ) : null} + {postVisibility === "custom" ? ( + setPostUsers(e.target.value)} + /> + ) : null} +
+ + {isAuthor && showPrivacy ? ( +
e.stopPropagation()}> +
+ + +
+ {postVisibility === "group" ? ( + setPostGroupId(e.target.value)} + /> + ) : null} + {postVisibility === "custom" ? ( + setPostUsers(e.target.value)} + /> + ) : null} + + {privacyError ?
{privacyError}
: null} + +
+ ) : null} ); } diff --git a/src/styles/island.css b/src/styles/island.css index 06129cf..0aa1ae4 100644 --- a/src/styles/island.css +++ b/src/styles/island.css @@ -128,6 +128,94 @@ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } +/* Profile panel */ +.island-profile-panel { + display: flex; + gap: 1rem; + align-items: center; + padding: 0.75rem 1rem; + border-radius: 14px; + border: 1px solid rgba(148, 163, 184, 0.2); + background: rgba(15, 23, 42, 0.6); + margin-bottom: 1.5rem; +} +.island-profile-avatar { + width: 64px; + height: 64px; + border-radius: 999px; + overflow: hidden; + border: 1px solid rgba(56, 189, 248, 0.6); + background: rgba(56, 189, 248, 0.2); + display: flex; + align-items: center; + justify-content: center; +} +.island-profile-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} +.island-profile-letter { + color: #e2f2ff; + font-weight: 800; + font-size: 1.4rem; +} +.island-profile-meta { + flex: 1; + min-width: 0; +} +.island-profile-name { + font-size: 0.95rem; + font-weight: 800; + color: #e2e8f0; + margin-bottom: 0.35rem; +} +.island-profile-actions { + display: flex; + flex-direction: column; + gap: 0.35rem; +} +.island-profile-row { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + color: #cbd5f5; +} +.island-profile-row select { + flex: 1; + background: rgba(15, 23, 42, 0.6); + border: 1px solid rgba(148, 163, 184, 0.4); + color: #e2e8f0; + border-radius: 999px; + padding: 0.2rem 0.6rem; + font-size: 0.72rem; +} +.island-profile-input { + width: 100%; + background: rgba(15, 23, 42, 0.6); + border: 1px solid rgba(148, 163, 184, 0.4); + color: #e2e8f0; + border-radius: 10px; + padding: 0.35rem 0.6rem; + font-size: 0.72rem; +} +.island-profile-btn { + align-self: flex-start; + border: 1px solid rgba(56, 189, 248, 0.6); + background: rgba(56, 189, 248, 0.2); + color: #e2f2ff; + font-weight: 800; + font-size: 0.72rem; + padding: 0.35rem 0.8rem; + border-radius: 999px; + cursor: pointer; +} +.island-profile-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + /* Canvas */ .island-viewer-canvas { diff --git a/src/styles/posts.css b/src/styles/posts.css index 93ece2f..cc19519 100644 --- a/src/styles/posts.css +++ b/src/styles/posts.css @@ -169,6 +169,55 @@ body[data-theme="light"] .sw-wall-image-fallback{ gap:.4rem; flex-wrap:wrap; } +.sw-wall-privacy{ + margin-top:.25rem; + padding:.5rem .6rem; + border-radius:12px; + border:1px solid rgba(148,163,184,0.35); + background: rgba(15,23,42,0.75); + display:flex; + flex-direction:column; + gap:.4rem; +} +.sw-wall-privacy-row{ + display:flex; + align-items:center; + gap:.5rem; + font-size:.72rem; + color:#cbd5f5; +} +.sw-wall-privacy-row label{ + white-space:nowrap; +} +.sw-wall-privacy-row select{ + flex:1; + background:#020617; + border-radius:999px; + border:1px solid #4b5563; + padding:.25rem .6rem; + font-size:.72rem; + color:#e5e7eb; +} +.sw-wall-privacy-input{ + width:100%; + border-radius:999px; + border:1px solid #4b5563; + padding:.3rem .6rem; + background:#020617; + color:#e5e7eb; + font-size:.72rem; +} +.sw-wall-privacy-check{ + display:flex; + align-items:center; + gap:.45rem; + font-size:.7rem; + color:#cbd5f5; +} +.sw-wall-privacy-error{ + font-size:.7rem; + color:#fca5a5; +} .sw-wall-sentinel{ height: 1px;