diff options
Diffstat (limited to 'front/src/components')
-rw-r--r-- | front/src/components/Avatar.tsx | 26 | ||||
-rw-r--r-- | front/src/components/Icon.tsx | 10 | ||||
-rw-r--r-- | front/src/components/modals/UserModal.tsx | 65 | ||||
-rw-r--r-- | front/src/components/post/Post.tsx | 1 | ||||
-rw-r--r-- | front/src/components/profile/Editor.tsx (renamed from front/src/components/ProfileEditor.tsx) | 104 | ||||
-rw-r--r-- | front/src/components/profile/Profile.tsx | 67 |
6 files changed, 199 insertions, 74 deletions
diff --git a/front/src/components/Avatar.tsx b/front/src/components/Avatar.tsx index 0f3dc90..a071655 100644 --- a/front/src/components/Avatar.tsx +++ b/front/src/components/Avatar.tsx @@ -1,20 +1,21 @@ import useLocalState from "@/state/state"; -import type { Ship } from "@/types/urbit"; import Sigil from "./Sigil"; -import ShipModal from "./modals/ShipModal"; import { isValidPatp } from "urbit-ob"; -import type { UserProfile } from "@/types/nostrill"; +import type { UserProfile, UserType } from "@/types/nostrill"; import Icon from "@/components/Icon"; +import UserModal from "./modals/UserModal"; export default function ({ - p, + user, + userString, size, color, noClickOnName, profile, picOnly = false, }: { - p: Ship; + user: UserType; + userString: string; size: number; color?: string; noClickOnName?: boolean; @@ -23,10 +24,11 @@ export default function ({ }) { const { setModal } = useLocalState((s) => ({ setModal: s.setModal })); // TODO revisit this when %whom updates + console.log({ profile }); const avatarInner = profile ? ( - <img src={profile.picture} /> - ) : isValidPatp(p) ? ( - <Sigil patp={p} size={size} bg={color} /> + <img src={profile.picture} width={size} height={size} /> + ) : "urbit" in user && isValidPatp(user.urbit) ? ( + <Sigil patp={user.urbit} size={size} bg={color} /> ) : ( <Icon name="comet" /> ); @@ -41,14 +43,18 @@ export default function ({ function openModal(e: React.MouseEvent) { if (noClickOnName) return; e.stopPropagation(); - setModal(<ShipModal ship={p} />); + setModal(<UserModal user={user} userString={userString} />); } const name = ( <div className="name cp" role="link" onMouseUp={openModal}> {profile ? ( <p>{profile.name}</p> + ) : "urbit" in user ? ( + <p className={"p-only" + tooLong(user.urbit)}> + {user.urbit.length > 28 ? "Anon" : user.urbit} + </p> ) : ( - <p className={"p-only" + tooLong(p)}>{p.length > 28 ? "Anon" : p}</p> + <p className={"p-only" + tooLong(user.nostr)}>{user.nostr}</p> )} </div> ); diff --git a/front/src/components/Icon.tsx b/front/src/components/Icon.tsx index a316e08..797a87b 100644 --- a/front/src/components/Icon.tsx +++ b/front/src/components/Icon.tsx @@ -65,7 +65,7 @@ interface IconProps { size?: number; className?: string; title?: string; - onClick?: (e?: React.MouseEvent) => void; + onClick?: (e: React.MouseEvent) => any; color?: "primary" | "text" | "textSecondary" | "textMuted" | "custom"; customColor?: string; } @@ -84,7 +84,11 @@ const Icon: React.FC<IconProps> = ({ // Simple filter based on theme - icons should match text const getFilter = () => { // For dark themes, invert the black SVGs to white - if (theme.name === "dark" || theme.name === "noir" || theme.name === "gruvbox") { + if ( + theme.name === "dark" || + theme.name === "noir" || + theme.name === "gruvbox" + ) { return "invert(1)"; } // For light themes with dark text, keep as is @@ -130,4 +134,4 @@ const Icon: React.FC<IconProps> = ({ ); }; -export default Icon;
\ No newline at end of file +export default Icon; diff --git a/front/src/components/modals/UserModal.tsx b/front/src/components/modals/UserModal.tsx new file mode 100644 index 0000000..6e3089d --- /dev/null +++ b/front/src/components/modals/UserModal.tsx @@ -0,0 +1,65 @@ +import Modal from "./Modal"; +import Avatar from "../Avatar"; +import Icon from "@/components/Icon"; +import useLocalState from "@/state/state"; +import { useLocation } from "wouter"; +import toast from "react-hot-toast"; +import type { UserType } from "@/types/nostrill"; + +export default function ({ + user, + userString, +}: { + user: UserType; + userString: string; +}) { + const { setModal, api, pubkey } = useLocalState((s) => ({ + setModal: s.setModal, + api: s.api, + pubkey: s.pubkey, + })); + const [_, navigate] = useLocation(); + function close() { + setModal(null); + } + const itsMe = + "urbit" in user + ? user.urbit === api?.airlock.our + : "nostr" in user + ? user.nostr === pubkey + : false; + async function copy(e: React.MouseEvent) { + e.stopPropagation(); + await navigator.clipboard.writeText(userString); + toast.success("Copied to clipboard"); + } + return ( + <Modal close={close}> + <div id="ship-modal"> + <div className="flex"> + <Avatar user={user} userString={userString} size={60} /> + <Icon + name="copy" + size={20} + className="copy-icon cp" + onClick={copy} + title="Copy ship name" + /> + </div> + <div className="buttons f1"> + <button onClick={() => navigate(`/feed/${userString}`)}>Feed</button> + <button onClick={() => navigate(`/pals/${userString}`)}> + Profile + </button> + {itsMe && ( + <> + <button onClick={() => navigate(`/chat/dm/${userString}`)}> + DM + </button> + </> + )} + </div> + </div> + </Modal> + ); +} diff --git a/front/src/components/post/Post.tsx b/front/src/components/post/Post.tsx index 277c119..2965040 100644 --- a/front/src/components/post/Post.tsx +++ b/front/src/components/post/Post.tsx @@ -22,6 +22,7 @@ export interface PostProps { profile?: UserProfile; } function Post(props: PostProps) { + console.log("post", props); const { poast } = props; if (!poast || poast.contents === null) { return null; diff --git a/front/src/components/ProfileEditor.tsx b/front/src/components/profile/Editor.tsx index 9a7493f..2e4aebc 100644 --- a/front/src/components/ProfileEditor.tsx +++ b/front/src/components/profile/Editor.tsx @@ -1,31 +1,37 @@ -import { useState, useEffect } from "react"; -import type { UserProfile } from "@/types/nostrill"; +import { useState } from "react"; +import type { UserProfile, UserType } from "@/types/nostrill"; import useLocalState from "@/state/state"; import Icon from "@/components/Icon"; import toast from "react-hot-toast"; -import Avatar from "./Avatar"; +import Avatar from "../Avatar"; interface ProfileEditorProps { - ship: string; + user: UserType; + userString: string; + profile: UserProfile | undefined; onSave?: () => void; } -const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => { +const ProfileEditor: React.FC<ProfileEditorProps> = ({ + user, + profile, + userString, + onSave, +}) => { const { api, profiles } = useLocalState((s) => ({ api: s.api, + pubkey: s.pubkey, 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 [name, setName] = useState(profile?.name || userString); + const [picture, setPicture] = useState(profile?.picture || ""); + const [about, setAbout] = useState(profile?.about || ""); const [customFields, setCustomFields] = useState< Array<{ key: string; value: string }> >( - Object.entries(existingProfile?.other || {}).map(([key, value]) => ({ + Object.entries(profile?.other || {}).map(([key, value]) => ({ key, value, })), @@ -33,21 +39,6 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => { 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: "" }]); }; @@ -77,7 +68,7 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => { } }); - const profile: UserProfile = { + const nprofile: UserProfile = { name, picture, about, @@ -86,7 +77,7 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => { // Call API to save profile if (api && typeof api.createProfile === "function") { - await api.createProfile(profile); + await api.createProfile(nprofile); } else { throw new Error("Profile update API not available"); } @@ -104,9 +95,9 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => { const handleCancel = () => { // Reset to original values - const profile = profiles.get(ship); + const profile = profiles.get(userString); if (profile) { - setName(profile.name || ""); + setName(profile.name || userString); setPicture(profile.picture || ""); setAbout(profile.about || ""); setCustomFields( @@ -118,39 +109,14 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => { } 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> - ); - } + console.log({ profile }); + console.log({ name, picture, customFields }); return ( <div className="profile-editor"> <div className="profile-header"> <h2>Edit Profile</h2> - {isOwnProfile && !isEditing && ( + {!isEditing && ( <button onClick={() => setIsEditing(true)} className="edit-btn"> <Icon name="settings" size={16} /> Edit @@ -181,7 +147,17 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => { placeholder="https://example.com/avatar.jpg" /> <div className="picture-preview"> - <Avatar p={ship} size={54} picOnly={true} /> + {picture ? ( + <img src={picture} /> + ) : ( + <Avatar + user={user} + userString={userString} + profile={profile} + size={120} + picOnly={true} + /> + )} </div> </div> @@ -252,11 +228,17 @@ const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => { ) : ( <div className="profile-view"> <div className="profile-picture"> - <Avatar p={ship} size={120} picOnly={true} /> + <Avatar + user={user} + userString={userString} + profile={profile} + size={120} + picOnly={true} + /> </div> <div className="profile-info"> - <h3>{name || ship}</h3> + <h3>{name}</h3> {about && <p className="profile-about">{about}</p>} {customFields.length > 0 && ( diff --git a/front/src/components/profile/Profile.tsx b/front/src/components/profile/Profile.tsx new file mode 100644 index 0000000..b5f22e9 --- /dev/null +++ b/front/src/components/profile/Profile.tsx @@ -0,0 +1,67 @@ +import "@/styles/Profile.css"; +import type { UserProfile, UserType } from "@/types/nostrill"; +import useLocalState from "@/state/state"; +import Avatar from "../Avatar"; +import ProfileEditor from "./Editor"; + +interface Props { + user: UserType; + userString: string; + isMe: boolean; + onSave?: () => void; +} + +const Loader: React.FC<Props> = (props) => { + const { profiles } = useLocalState((s) => ({ + profiles: s.profiles, + })); + const profile = profiles.get(props.userString); + + if (props.isMe) return <ProfileEditor {...props} profile={profile} />; + else return <Profile profile={profile} {...props} />; +}; +function Profile({ + user, + userString, + profile, +}: { + user: UserType; + userString: string; + profile: UserProfile | undefined; +}) { + // Initialize state with existing profile or defaults + + // View-only mode for other users' profiles - no editing allowed + const customFields = profile?.other ? Object.entries(profile.other) : []; + return ( + <div className="profile view-mode"> + <div className="profile-picture"> + <Avatar + user={user} + userString={userString} + size={120} + picOnly={true} + profile={profile} + /> + </div> + <div className="profile-info"> + <h2>{profile?.name || userString}</h2> + {profile?.about && <p className="profile-about">{profile.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> + ); +} + +export default Loader; |