feat: new user flow profile editor
This commit is contained in:
@ -8,6 +8,7 @@
|
||||
background-clip: content-box, border-box;
|
||||
background-size: cover;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
.avatar[data-domain="snort.social"] {
|
||||
|
49
packages/app/src/Element/AvatarEditor.tsx
Normal file
49
packages/app/src/Element/AvatarEditor.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import Icon from "Icons/Icon";
|
||||
import { useState } from "react";
|
||||
import useFileUpload from "Upload";
|
||||
import { openFile, unwrap } from "Util";
|
||||
|
||||
interface AvatarEditorProps {
|
||||
picture?: string;
|
||||
onPictureChange?: (newPicture: string) => void;
|
||||
}
|
||||
|
||||
export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorProps) {
|
||||
const uploader = useFileUpload();
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function uploadFile() {
|
||||
setError("");
|
||||
try {
|
||||
const f = await openFile();
|
||||
if (f) {
|
||||
const rsp = await uploader.upload(f, f.name);
|
||||
console.log(rsp);
|
||||
if (typeof rsp?.error === "string") {
|
||||
setError(`Upload failed: ${rsp.error}`);
|
||||
} else {
|
||||
onPictureChange?.(unwrap(rsp.url));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(`Upload failed: ${e.message}`);
|
||||
} else {
|
||||
setError(`Upload failed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex f-center">
|
||||
<div style={{ backgroundImage: `url(${picture})` }} className="avatar">
|
||||
<div className={`edit${picture ? "" : " new"}`} onClick={() => uploadFile().catch(console.error)}>
|
||||
<Icon name={picture ? "edit" : "camera-plus"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && <b className="error">{error}</b>}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
import { ReactNode } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export interface FollowListBaseProps {
|
||||
pubkeys: HexKey[];
|
||||
@ -26,7 +26,7 @@ export default function FollowListBase({ pubkeys, title, showFollowAll, showAbou
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<>
|
||||
{(showFollowAll ?? true) && (
|
||||
<div className="flex mt10 mb10">
|
||||
<div className="f-grow bold">{title}</div>
|
||||
@ -38,6 +38,6 @@ export default function FollowListBase({ pubkeys, title, showFollowAll, showAbou
|
||||
{pubkeys?.map(a => (
|
||||
<ProfilePreview pubkey={a} key={a} options={{ about: showAbout }} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
.hashtag {
|
||||
color: var(--highlight);
|
||||
}
|
||||
|
||||
.hashtag > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -8,6 +8,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link-preview-container > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link-preview-title {
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
|
@ -264,14 +264,7 @@ export default function ProfilePage() {
|
||||
}
|
||||
case FOLLOWS: {
|
||||
if (isMe) {
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => navigate("/new/import")} className="mb10">
|
||||
<FormattedMessage defaultMessage="Find Twitter follows" />
|
||||
</button>
|
||||
<FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} />;
|
||||
</>
|
||||
);
|
||||
return <FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} />;
|
||||
} else {
|
||||
return <FollowsTab id={id} />;
|
||||
}
|
||||
@ -379,7 +372,7 @@ export default function ProfilePage() {
|
||||
{isMe && blocked.length > 0 && renderTab(ProfileTab.Blocked)}
|
||||
</div>
|
||||
</div>
|
||||
{tabContent()}
|
||||
<div className="main-content">{tabContent()}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ export default function DiscoverFollows() {
|
||||
<h3>
|
||||
<FormattedMessage {...messages.PopularAccounts} />
|
||||
</h3>
|
||||
<div>{sortedReccomends.length > 0 && <FollowListBase pubkeys={sortedReccomends} showAbout={true} />}</div>
|
||||
{sortedReccomends.length > 0 && <FollowListBase pubkeys={sortedReccomends} showAbout={true} />}
|
||||
<TrendingUsers />
|
||||
</div>
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ import { hexToMnemonic } from "nip6";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
import { PROFILE } from ".";
|
||||
|
||||
const WhatIsSnort = () => {
|
||||
return (
|
||||
@ -107,7 +108,7 @@ export default function NewUserFlow() {
|
||||
</h2>
|
||||
<Copy text={hexToMnemonic(generatedEntropy ?? "")} />
|
||||
<div className="next-actions">
|
||||
<button type="button" onClick={() => navigate("/new/username")}>
|
||||
<button type="button" onClick={() => navigate(PROFILE)}>
|
||||
<FormattedMessage {...messages.KeysSaved} />{" "}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,26 +1,47 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import Logo from "Element/Logo";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { mapEventToProfile, UserCache } from "Cache";
|
||||
import AvatarEditor from "Element/AvatarEditor";
|
||||
|
||||
import messages from "./messages";
|
||||
import { DISCOVER } from ".";
|
||||
|
||||
export default function NewUserName() {
|
||||
export default function ProfileSetup() {
|
||||
const login = useLogin();
|
||||
const myProfile = useUserProfile(login.publicKey);
|
||||
const [username, setUsername] = useState("");
|
||||
const [picture, setPicture] = useState("");
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const nextPage = "/new/discover";
|
||||
useEffect(() => {
|
||||
if (myProfile) {
|
||||
setUsername(myProfile.name ?? "");
|
||||
setPicture(myProfile.picture ?? "");
|
||||
}
|
||||
}, [myProfile]);
|
||||
|
||||
const onNext = async () => {
|
||||
if (username.length > 0 && publisher) {
|
||||
const ev = await publisher.metadata({ name: username });
|
||||
if ((username.length > 0 || picture.length > 0) && publisher) {
|
||||
const ev = await publisher.metadata({
|
||||
...myProfile,
|
||||
name: username,
|
||||
picture,
|
||||
});
|
||||
publisher.broadcast(ev);
|
||||
const profile = mapEventToProfile(ev);
|
||||
if (profile) {
|
||||
UserCache.set(profile);
|
||||
}
|
||||
}
|
||||
navigate(nextPage);
|
||||
navigate(DISCOVER);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -30,13 +51,14 @@ export default function NewUserName() {
|
||||
<div className="progress progress-second"></div>
|
||||
</div>
|
||||
<h1>
|
||||
<FormattedMessage {...messages.PickUsername} />
|
||||
<FormattedMessage defaultMessage="Setup profile" />
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage {...messages.UsernameHelp} />
|
||||
</p>
|
||||
<h2>
|
||||
<FormattedMessage {...messages.Username} />
|
||||
<FormattedMessage defaultMessage="Profile picture" />
|
||||
</h2>
|
||||
<AvatarEditor picture={picture} onPictureChange={p => setPicture(p)} />
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="Username" />
|
||||
</h2>
|
||||
<input
|
||||
className="username"
|
||||
@ -49,7 +71,7 @@ export default function NewUserName() {
|
||||
<FormattedMessage defaultMessage="You can change your username at any point." />
|
||||
</div>
|
||||
<div className="next-actions">
|
||||
<button type="button" className="transparent" onClick={() => navigate(nextPage)}>
|
||||
<button type="button" className="transparent" onClick={() => navigate(DISCOVER)}>
|
||||
<FormattedMessage {...messages.Skip} />
|
||||
</button>
|
||||
<button type="button" onClick={onNext}>
|
@ -2,15 +2,15 @@ import "./index.css";
|
||||
import { RouteObject } from "react-router-dom";
|
||||
|
||||
import GetVerified from "Pages/new/GetVerified";
|
||||
import NewUserName from "Pages/new/NewUsername";
|
||||
import ProfileSetup from "Pages/new/ProfileSetup";
|
||||
import NewUserFlow from "Pages/new/NewUserFlow";
|
||||
import ImportFollows from "Pages/new/ImportFollows";
|
||||
import DiscoverFollows from "Pages/new/DiscoverFollows";
|
||||
|
||||
const USERNAME = "/new/username";
|
||||
const IMPORT = "/new/import";
|
||||
const DISCOVER = "/new/discover";
|
||||
const VERIFY = "/new/verify";
|
||||
export const PROFILE = "/new/profile";
|
||||
export const IMPORT = "/new/import";
|
||||
export const DISCOVER = "/new/discover";
|
||||
export const VERIFY = "/new/verify";
|
||||
|
||||
export const NewUserRoutes: RouteObject[] = [
|
||||
{
|
||||
@ -18,8 +18,8 @@ export const NewUserRoutes: RouteObject[] = [
|
||||
element: <NewUserFlow />,
|
||||
},
|
||||
{
|
||||
path: USERNAME,
|
||||
element: <NewUserName />,
|
||||
path: PROFILE,
|
||||
element: <ProfileSetup />,
|
||||
},
|
||||
{
|
||||
path: IMPORT,
|
||||
|
@ -37,10 +37,6 @@ export default defineMessages({
|
||||
ExtensionsNostr: { defaultMessage: `You can also use these extensions to login to most Nostr sites.` },
|
||||
ImproveSecurity: { defaultMessage: "Improve login security with browser extensions" },
|
||||
PickUsername: { defaultMessage: "Pick a username" },
|
||||
UsernameHelp: {
|
||||
defaultMessage:
|
||||
"On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step.",
|
||||
},
|
||||
Username: { defaultMessage: "Username" },
|
||||
UsernamePlaceholder: { defaultMessage: "e.g. Jack" },
|
||||
PopularAccounts: { defaultMessage: "Follow some popular accounts" },
|
||||
|
@ -22,29 +22,31 @@
|
||||
|
||||
.settings .image-setting {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.settings .image-setting > div:first-child {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.settings .avatar,
|
||||
.settings .banner {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.settings .avatar .edit,
|
||||
.settings .banner .edit {
|
||||
.avatar .edit,
|
||||
.banner .edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background-color: var(--bg-color);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.settings .avatar .edit:hover {
|
||||
.avatar .edit.new {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.avatar .edit:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ import { mapEventToProfile, UserCache } from "Cache";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
import AvatarEditor from "Element/AvatarEditor";
|
||||
|
||||
export interface ProfileSettingsProps {
|
||||
avatar?: boolean;
|
||||
@ -36,7 +37,6 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
const [website, setWebsite] = useState<string>();
|
||||
const [nip05, setNip05] = useState<string>();
|
||||
const [lud16, setLud16] = useState<string>();
|
||||
const [reactions, setReactions] = useState<boolean>();
|
||||
|
||||
const avatarPicture = (picture?.length ?? 0) === 0 ? Nostrich : picture;
|
||||
|
||||
@ -98,13 +98,6 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function setNewAvatar() {
|
||||
const rsp = await uploadFile();
|
||||
if (rsp) {
|
||||
setPicture(rsp);
|
||||
}
|
||||
}
|
||||
|
||||
async function setNewBanner() {
|
||||
const rsp = await uploadFile();
|
||||
if (rsp) {
|
||||
@ -189,11 +182,7 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
<div>
|
||||
<FormattedMessage {...messages.Avatar} />:
|
||||
</div>
|
||||
<div style={{ backgroundImage: `url(${avatarPicture})` }} className="avatar">
|
||||
<div className="edit" onClick={() => setNewAvatar()}>
|
||||
<FormattedMessage {...messages.Edit} />
|
||||
</div>
|
||||
</div>
|
||||
<AvatarEditor picture={avatarPicture} onPictureChange={p => setPicture(p)} />
|
||||
</div>
|
||||
)}
|
||||
{(props.banner ?? true) && (
|
||||
|
@ -374,7 +374,6 @@ input:disabled {
|
||||
a {
|
||||
color: inherit;
|
||||
line-height: 1.3em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.ext {
|
||||
|
Reference in New Issue
Block a user