summaryrefslogtreecommitdiff
path: root/front/src/components/ProfileEditor.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'front/src/components/ProfileEditor.tsx')
-rw-r--r--front/src/components/ProfileEditor.tsx280
1 files changed, 280 insertions, 0 deletions
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;