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 (
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;