From 985fa2f7c99832cdf3c3351d2273c8fd05402b78 Mon Sep 17 00:00:00 2001
From: polwex <polwex@sortug.com>
Date: Wed, 17 Sep 2025 21:45:18 +0700
Subject: basic comms working

---
 front/src/components/ProfileEditor.tsx | 280 +++++++++++++++++++++++++++++++++
 1 file changed, 280 insertions(+)
 create mode 100644 front/src/components/ProfileEditor.tsx

(limited to 'front/src/components/ProfileEditor.tsx')

diff --git a/front/src/components/ProfileEditor.tsx b/front/src/components/ProfileEditor.tsx
new file mode 100644
index 0000000..9a7493f
--- /dev/null
+++ b/front/src/components/ProfileEditor.tsx
@@ -0,0 +1,280 @@
+import { useState, useEffect } from "react";
+import type { UserProfile } from "@/types/nostrill";
+import useLocalState from "@/state/state";
+import Icon from "@/components/Icon";
+import toast from "react-hot-toast";
+import Avatar from "./Avatar";
+
+interface ProfileEditorProps {
+  ship: string;
+  onSave?: () => void;
+}
+
+const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => {
+  const { api, profiles } = useLocalState((s) => ({
+    api: s.api,
+    profiles: s.profiles,
+  }));
+  const isOwnProfile = ship === api?.airlock.our;
+
+  // Initialize state with existing profile or defaults
+  const existingProfile = profiles.get(ship);
+  const [name, setName] = useState(existingProfile?.name || "");
+  const [picture, setPicture] = useState(existingProfile?.picture || "");
+  const [about, setAbout] = useState(existingProfile?.about || "");
+  const [customFields, setCustomFields] = useState<
+    Array<{ key: string; value: string }>
+  >(
+    Object.entries(existingProfile?.other || {}).map(([key, value]) => ({
+      key,
+      value,
+    })),
+  );
+  const [isEditing, setIsEditing] = useState(false);
+  const [isSaving, setIsSaving] = useState(false);
+
+  useEffect(() => {
+    const profile = profiles.get(ship);
+    if (profile) {
+      setName(profile.name || "");
+      setPicture(profile.picture || "");
+      setAbout(profile.about || "");
+      setCustomFields(
+        Object.entries(profile.other || {}).map(([key, value]) => ({
+          key,
+          value,
+        })),
+      );
+    }
+  }, [ship, profiles]);
+
+  const handleAddCustomField = () => {
+    setCustomFields([...customFields, { key: "", value: "" }]);
+  };
+
+  const handleUpdateCustomField = (
+    index: number,
+    field: "key" | "value",
+    newValue: string,
+  ) => {
+    const updated = [...customFields];
+    updated[index][field] = newValue;
+    setCustomFields(updated);
+  };
+
+  const handleRemoveCustomField = (index: number) => {
+    setCustomFields(customFields.filter((_, i) => i !== index));
+  };
+
+  const handleSave = async () => {
+    setIsSaving(true);
+    try {
+      // Convert custom fields array to object
+      const other: Record<string, string> = {};
+      customFields.forEach(({ key, value }) => {
+        if (key.trim()) {
+          other[key.trim()] = value;
+        }
+      });
+
+      const profile: UserProfile = {
+        name,
+        picture,
+        about,
+        other,
+      };
+
+      // Call API to save profile
+      if (api && typeof api.createProfile === "function") {
+        await api.createProfile(profile);
+      } else {
+        throw new Error("Profile update API not available");
+      }
+
+      toast.success("Profile updated successfully");
+      setIsEditing(false);
+      onSave?.();
+    } catch (error) {
+      toast.error("Failed to update profile");
+      console.error("Failed to save profile:", error);
+    } finally {
+      setIsSaving(false);
+    }
+  };
+
+  const handleCancel = () => {
+    // Reset to original values
+    const profile = profiles.get(ship);
+    if (profile) {
+      setName(profile.name || "");
+      setPicture(profile.picture || "");
+      setAbout(profile.about || "");
+      setCustomFields(
+        Object.entries(profile.other || {}).map(([key, value]) => ({
+          key,
+          value,
+        })),
+      );
+    }
+    setIsEditing(false);
+  };
+
+  if (!isOwnProfile) {
+    // View-only mode for other users' profiles - no editing allowed
+    return (
+      <div className="profile-editor view-mode">
+        <div className="profile-picture">
+          <Avatar p={ship} size={120} picOnly={true} />
+        </div>
+        <div className="profile-info">
+          <h2>{name || ship}</h2>
+          {about && <p className="profile-about">{about}</p>}
+
+          {customFields.length > 0 && (
+            <div className="profile-custom-fields">
+              <h4>Additional Info</h4>
+              {customFields.map(({ key, value }, index) => (
+                <div key={index} className="custom-field-view">
+                  <span className="field-key">{key}:</span>
+                  <span className="field-value">{value}</span>
+                </div>
+              ))}
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="profile-editor">
+      <div className="profile-header">
+        <h2>Edit Profile</h2>
+        {isOwnProfile && !isEditing && (
+          <button onClick={() => setIsEditing(true)} className="edit-btn">
+            <Icon name="settings" size={16} />
+            Edit
+          </button>
+        )}
+      </div>
+
+      {isEditing ? (
+        <div className="profile-form">
+          <div className="form-group">
+            <label htmlFor="name">Display Name</label>
+            <input
+              id="name"
+              type="text"
+              value={name}
+              onChange={(e) => setName(e.target.value)}
+              placeholder="Your display name"
+            />
+          </div>
+
+          <div className="form-group">
+            <label htmlFor="picture">Profile Picture URL</label>
+            <input
+              id="picture"
+              type="url"
+              value={picture}
+              onChange={(e) => setPicture(e.target.value)}
+              placeholder="https://example.com/avatar.jpg"
+            />
+            <div className="picture-preview">
+              <Avatar p={ship} size={54} picOnly={true} />
+            </div>
+          </div>
+
+          <div className="form-group">
+            <label htmlFor="about">About</label>
+            <textarea
+              id="about"
+              value={about}
+              onChange={(e) => setAbout(e.target.value)}
+              placeholder="Tell us about yourself..."
+              rows={4}
+            />
+          </div>
+
+          <div className="form-group custom-fields">
+            <label>Custom Fields</label>
+            {customFields.map((field, index) => (
+              <div key={index} className="custom-field-row">
+                <input
+                  type="text"
+                  value={field.key}
+                  onChange={(e) =>
+                    handleUpdateCustomField(index, "key", e.target.value)
+                  }
+                  placeholder="Field name"
+                  className="field-key-input"
+                />
+                <input
+                  type="text"
+                  value={field.value}
+                  onChange={(e) =>
+                    handleUpdateCustomField(index, "value", e.target.value)
+                  }
+                  placeholder="Field value"
+                  className="field-value-input"
+                />
+                <button
+                  onClick={() => handleRemoveCustomField(index)}
+                  className="remove-field-btn"
+                  title="Remove field"
+                >
+                  ×
+                </button>
+              </div>
+            ))}
+            <button onClick={handleAddCustomField} className="add-field-btn">
+              + Add Custom Field
+            </button>
+          </div>
+
+          <div className="form-actions">
+            <button
+              onClick={handleSave}
+              disabled={isSaving}
+              className="save-btn"
+            >
+              {isSaving ? "Saving..." : "Save Profile"}
+            </button>
+            <button
+              onClick={handleCancel}
+              disabled={isSaving}
+              className="cancel-btn"
+            >
+              Cancel
+            </button>
+          </div>
+        </div>
+      ) : (
+        <div className="profile-view">
+          <div className="profile-picture">
+            <Avatar p={ship} size={120} picOnly={true} />
+          </div>
+
+          <div className="profile-info">
+            <h3>{name || ship}</h3>
+            {about && <p className="profile-about">{about}</p>}
+
+            {customFields.length > 0 && (
+              <div className="profile-custom-fields">
+                <h4>Additional Info</h4>
+                {customFields.map(({ key, value }, index) => (
+                  <div key={index} className="custom-field-view">
+                    <span className="field-key">{key}:</span>
+                    <span className="field-value">{value}</span>
+                  </div>
+                ))}
+              </div>
+            )}
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default ProfileEditor;
-- 
cgit v1.2.3

