summaryrefslogtreecommitdiff
path: root/front/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'front/src/components')
-rw-r--r--front/src/components/Avatar.tsx26
-rw-r--r--front/src/components/Icon.tsx10
-rw-r--r--front/src/components/modals/UserModal.tsx65
-rw-r--r--front/src/components/post/Post.tsx1
-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.tsx67
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;