diff options
author | polwex <polwex@sortug.com> | 2025-09-17 21:45:18 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-09-17 21:45:18 +0700 |
commit | 985fa2f7c99832cdf3c3351d2273c8fd05402b78 (patch) | |
tree | bc727486a89ad05e588754f8de8b1096400a3d31 /front/src/components/ProfileEditor.tsx | |
parent | f0df4c7297a05bd592d8717b8997284c80fd0500 (diff) |
basic comms working
Diffstat (limited to 'front/src/components/ProfileEditor.tsx')
-rw-r--r-- | front/src/components/ProfileEditor.tsx | 280 |
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; |