From 5d0076dd62f0055d0280186182fdb0d3a409b6af Mon Sep 17 00:00:00 2001 From: Davinci Date: Tue, 21 May 2024 09:07:52 +0200 Subject: [PATCH 1/4] feat: added profile view --- src/pages/profile/index.tsx | 343 +++++++----------- src/pages/profile/style.module.scss | 56 ++- src/pages/settings/profile/index.tsx | 357 +++++++++++++++++++ src/pages/settings/profile/style.module.scss | 23 ++ src/routes/index.tsx | 11 +- 5 files changed, 551 insertions(+), 239 deletions(-) create mode 100644 src/pages/settings/profile/index.tsx create mode 100644 src/pages/settings/profile/style.module.scss diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 318f115..a7da30f 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -1,50 +1,43 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy' import { + Box, IconButton, - InputProps, - List, - ListItem, - ListSubheader, - TextField, - Tooltip, + SxProps, Typography, - useTheme + useTheme, + Theme } from '@mui/material' -import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' -import { useEffect, useMemo, useRef, useState } from 'react' -import { Link, useParams } from 'react-router-dom' +import { nip19, VerifiedEvent } from 'nostr-tools' +import { useEffect, useMemo, useState } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' import { toast } from 'react-toastify' -import { MetadataController, NostrController } from '../../controllers' +import { MetadataController } from '../../controllers' import { NostrJoiningBlock, ProfileMetadata } from '../../types' import styles from './style.module.scss' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer' -import { LoadingButton } from '@mui/lab' -import { Dispatch } from '../../store/store' -import { setMetadataEvent } from '../../store/actions' +import { getRoboHashPicture, shorten } from '../../utils' +import { truncate } from 'lodash' +import { getProfileSettingsRoute } from '../../routes' +import EditIcon from '@mui/icons-material/Edit'; +import LinkIcon from '@mui/icons-material/Link'; +import RestoreIcon from '@mui/icons-material/Restore'; import { LoadingSpinner } from '../../components/LoadingSpinner' -import { LoginMethods } from '../../store/auth/types' -import { SmartToy } from '@mui/icons-material' -import { getRoboHashPicture } from '../../utils' export const ProfilePage = () => { + const navigate = useNavigate() const theme = useTheme() const { npub } = useParams() - const dispatch: Dispatch = useDispatch() - const metadataController = useMemo(() => new MetadataController(), []) - const nostrController = NostrController.getInstance() const [pubkey, setPubkey] = useState() const [nostrJoiningBlock, setNostrJoiningBlock] = useState(null) const [profileMetadata, setProfileMetadata] = useState() - const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const metadataState = useSelector((state: State) => state.metadata) - const keys = useSelector((state: State) => state.auth?.keyPair) - const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth) + const { usersPubkey } = useSelector((state: State) => state.auth) const userRobotImage = useSelector((state: State) => state.userRobotImage) const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false) @@ -52,8 +45,6 @@ export const ProfilePage = () => { const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc] = useState('Fetching metadata') - const robotSet = useRef(1) - useEffect(() => { if (npub) { try { @@ -116,133 +107,30 @@ export const ProfilePage = () => { } }, [isUsersOwnProfile, metadataState, pubkey, metadataController]) - const editItem = ( - key: keyof ProfileMetadata, - label: string, - multiline = false, - rows = 1, - inputProps?: InputProps - ) => ( - - ) => { - const { value } = event.target + const textElementWithCopyIcon = (text: string) => { + const onClick = () => { + navigator.clipboard.writeText(text) - setProfileMetadata((prev) => ({ - ...prev, - [key]: value - })) - }} - /> - - ) - - const copyItem = ( - value: string, - label: string, - copyValue?: string, - isPassword = false - ) => ( - { - navigator.clipboard.writeText(copyValue || value) - - toast.success('Copied to clipboard', { - autoClose: 1000, - hideProgressBar: true - }) - }} - > - - }} - /> - - ) - - const handleSaveMetadata = async () => { - setSavingProfileMetadata(true) - - const content = JSON.stringify(profileMetadata) - - // We need to omit cachedAt and create new event - // Relay will reject if created_at is too late - const updatedMetadataState: UnsignedEvent = { - content: content, - created_at: Math.round(Date.now() / 1000), - kind: kinds.Metadata, - pubkey: pubkey!, - tags: metadataState?.tags || [] - } - - const signedEvent = await nostrController - .signEvent(updatedMetadataState) - .catch((error) => { - toast.error(`Error saving profile metadata. ${error}`) + toast.success('Copied to clipboard', { + autoClose: 1000, + hideProgressBar: true }) - - if (signedEvent) { - if (!metadataController.validate(signedEvent)) { - toast.error(`Metadata is not valid.`) - } - - await metadataController.publishMetadataEvent(signedEvent) - - dispatch(setMetadataEvent(signedEvent)) } - setSavingProfileMetadata(false) - } - - /** - * Called by clicking on the robot icon inside Picture URL input - * On every click, next robohash set will be generated. - * There are 5 sets at the moment, after 5th set function will start over from set 1. - */ - const generateRobotAvatar = () => { - robotSet.current++ - if (robotSet.current > 5) robotSet.current = 1 - - const robotAvatarLink = getRoboHashPicture(npub!, robotSet.current) - - setProfileMetadata((prev) => ({ - ...prev, - picture: robotAvatarLink - })) - } - - /** - * - * @returns robohash generate button, loading spinner or no button - */ - const robohashButton = () => { return ( - - - - - + + {shorten(text)} + + ) } + const titleElement = (text: string, sx?: SxProps) => ( + + {text} + + ) + /** * Handles the logic for Image URL. * If no picture in kind 0 found - use robohash avatar @@ -250,6 +138,8 @@ export const ProfilePage = () => { * @returns robohash image url */ const getProfileImage = (metadata: ProfileMetadata) => { + if (!metadata) return '' + if (!isUsersOwnProfile) { return metadata.picture || getRoboHashPicture(npub!) } @@ -262,96 +152,101 @@ export const ProfilePage = () => { return ( <> {isLoading && } -
- - Profile Settings - - } - > - {profileMetadata && ( -
- + + +
{ - event.target.src = getRoboHashPicture(npub!) - }} - className={styles.img} - src={getProfileImage(profileMetadata)} - alt="Profile Image" + className={styles['image-placeholder']} + src={getProfileImage(profileMetadata!)} /> - - {nostrJoiningBlock && ( - + + + + + - On nostr since {nostrJoiningBlock.block.toLocaleString()} - - )} - + {profileMetadata && + titleElement( + truncate( + profileMetadata.display_name || + profileMetadata.name || + pubkey, + { + length: 16 + } + ), + { marginRight: 1 } + )} + + + {textElementWithCopyIcon(pubkey || '')} + + + {profileMetadata?.nip05 && + textElementWithCopyIcon(profileMetadata.nip05)} + + - {editItem('picture', 'Picture URL', undefined, undefined, { - endAdornment: isUsersOwnProfile ? robohashButton() : undefined - })} + + {profileMetadata?.website && ( + + + {profileMetadata.website} + + )} + - {editItem('name', 'Username')} - {editItem('display_name', 'Display Name')} - {editItem('nip05', 'Nostr Address (nip05)')} - {editItem('lud16', 'Lightning Address (lud16)')} - {editItem('about', 'About', true, 4)} - {editItem('website', 'Website')} + + {nostrJoiningBlock && ( + + + On nostr since {nostrJoiningBlock.block.toLocaleString()} + + )} + + + + {isUsersOwnProfile && ( - <> - {usersPubkey && - copyItem(nip19.npubEncode(usersPubkey), 'Public Key')} - {loginMethod === LoginMethods.privateKey && - keys && - keys.private && - copyItem( - '••••••••••••••••••••••••••••••••••••••••••••••••••', - 'Private Key', - keys.private - )} - + navigate(getProfileSettingsRoute(pubkey))}> + + )} -
- )} - - {isUsersOwnProfile && ( - - SAVE - - )} -
+ + + + {profileMetadata?.about && ( + + {profileMetadata.about} + + )} + + + )} ) } diff --git a/src/pages/profile/style.module.scss b/src/pages/profile/style.module.scss index d5d3282..3e1afcc 100644 --- a/src/pages/profile/style.module.scss +++ b/src/pages/profile/style.module.scss @@ -1,23 +1,51 @@ -.container { +.upper { display: flex; - flex-direction: column; - gap: 25px; + padding-top: 15px; } -.textField { - width: 100%; +.container { + color: black } -.subHeader { - border-bottom: 0.5px solid; +.left { + margin-right: 10px; } -.img { - max-height: 40%; - max-width: 40%; -} - -.copyItem { +.right { margin-left: 10px; - color: #34495e; } + +.imageWrapper { + overflow: hidden; + border-radius: 50%; + width: 80px; + height: 80px; + display: flex; + justify-content: center; + align-items: center; +} + +.image-placeholder { + width: 150px; +} + +.copyIcon { + font-size: 1.1rem !important; + margin-left: 5px; +} + +.website { + // margin-top: 10px !important; + margin-bottom: 15px 0 !important; +} + +.captionWrapper { + display: flex; + align-items: center; +} + +.captionIcon { + color: #15999b; + margin-right: 10px; + font-size: 12px; +} \ No newline at end of file diff --git a/src/pages/settings/profile/index.tsx b/src/pages/settings/profile/index.tsx new file mode 100644 index 0000000..86799ed --- /dev/null +++ b/src/pages/settings/profile/index.tsx @@ -0,0 +1,357 @@ +import ContentCopyIcon from '@mui/icons-material/ContentCopy' +import { + IconButton, + InputProps, + List, + ListItem, + ListSubheader, + TextField, + Tooltip, + Typography, + useTheme +} from '@mui/material' +import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' +import { useEffect, useMemo, useRef, useState } from 'react' +import { Link, useParams } from 'react-router-dom' +import { toast } from 'react-toastify' +import { MetadataController, NostrController } from '../../../controllers' +import { NostrJoiningBlock, ProfileMetadata } from '../../../types' +import styles from './style.module.scss' +import { useDispatch, useSelector } from 'react-redux' +import { State } from '../../../store/rootReducer' +import { LoadingButton } from '@mui/lab' +import { Dispatch } from '../../../store/store' +import { setMetadataEvent } from '../../../store/actions' +import { LoadingSpinner } from '../../../components/LoadingSpinner' +import { LoginMethods } from '../../../store/auth/types' +import { SmartToy } from '@mui/icons-material' +import { getRoboHashPicture } from '../../../utils' + +export const ProfileSettingsPage = () => { + const theme = useTheme() + + const { npub } = useParams() + + const dispatch: Dispatch = useDispatch() + + const metadataController = useMemo(() => new MetadataController(), []) + const nostrController = NostrController.getInstance() + + const [pubkey, setPubkey] = useState() + const [nostrJoiningBlock, setNostrJoiningBlock] = + useState(null) + const [profileMetadata, setProfileMetadata] = useState() + const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) + const metadataState = useSelector((state: State) => state.metadata) + const keys = useSelector((state: State) => state.auth?.keyPair) + const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth) + const userRobotImage = useSelector((state: State) => state.userRobotImage) + + const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false) + + const [isLoading, setIsLoading] = useState(true) + const [loadingSpinnerDesc] = useState('Fetching metadata') + + const robotSet = useRef(1) + + useEffect(() => { + if (npub) { + try { + const hexPubkey = nip19.decode(npub).data as string + setPubkey(hexPubkey) + + if (hexPubkey === usersPubkey) setIsUsersOwnProfile(true) + } catch (error) { + toast.error('Error occurred in decoding npub' + error) + } + } + }, [npub, usersPubkey]) + + useEffect(() => { + if (pubkey) { + metadataController + .getNostrJoiningBlockNumber(pubkey) + .then((res) => { + setNostrJoiningBlock(res) + }) + .catch((err) => { + // todo: handle error + console.log('err :>> ', err) + }) + } + + if (isUsersOwnProfile && metadataState) { + const metadataContent = metadataController.extractProfileMetadataContent( + metadataState as VerifiedEvent + ) + if (metadataContent) { + setProfileMetadata(metadataContent) + setIsLoading(false) + } + + return + } + + if (pubkey) { + const getMetadata = async (pubkey: string) => { + const metadataEvent = await metadataController + .findMetadata(pubkey) + .catch((err) => { + toast.error(err) + return null + }) + + if (metadataEvent) { + const metadataContent = + metadataController.extractProfileMetadataContent(metadataEvent) + if (metadataContent) { + setProfileMetadata(metadataContent) + } + } + + setIsLoading(false) + } + + getMetadata(pubkey) + } + }, [isUsersOwnProfile, metadataState, pubkey, metadataController]) + + const editItem = ( + key: keyof ProfileMetadata, + label: string, + multiline = false, + rows = 1, + inputProps?: InputProps + ) => ( + + ) => { + const { value } = event.target + + setProfileMetadata((prev) => ({ + ...prev, + [key]: value + })) + }} + /> + + ) + + const copyItem = ( + value: string, + label: string, + copyValue?: string, + isPassword = false + ) => ( + { + navigator.clipboard.writeText(copyValue || value) + + toast.success('Copied to clipboard', { + autoClose: 1000, + hideProgressBar: true + }) + }} + > + + }} + /> + + ) + + const handleSaveMetadata = async () => { + setSavingProfileMetadata(true) + + const content = JSON.stringify(profileMetadata) + + // We need to omit cachedAt and create new event + // Relay will reject if created_at is too late + const updatedMetadataState: UnsignedEvent = { + content: content, + created_at: Math.round(Date.now() / 1000), + kind: kinds.Metadata, + pubkey: pubkey!, + tags: metadataState?.tags || [] + } + + const signedEvent = await nostrController + .signEvent(updatedMetadataState) + .catch((error) => { + toast.error(`Error saving profile metadata. ${error}`) + }) + + if (signedEvent) { + if (!metadataController.validate(signedEvent)) { + toast.error(`Metadata is not valid.`) + } + + await metadataController.publishMetadataEvent(signedEvent) + + dispatch(setMetadataEvent(signedEvent)) + } + + setSavingProfileMetadata(false) + } + + /** + * Called by clicking on the robot icon inside Picture URL input + * On every click, next robohash set will be generated. + * There are 5 sets at the moment, after 5th set function will start over from set 1. + */ + const generateRobotAvatar = () => { + robotSet.current++ + if (robotSet.current > 5) robotSet.current = 1 + + const robotAvatarLink = getRoboHashPicture(npub!, robotSet.current) + + setProfileMetadata((prev) => ({ + ...prev, + picture: robotAvatarLink + })) + } + + /** + * + * @returns robohash generate button, loading spinner or no button + */ + const robohashButton = () => { + return ( + + + + + + ) + } + + /** + * Handles the logic for Image URL. + * If no picture in kind 0 found - use robohash avatar + * + * @returns robohash image url + */ + const getProfileImage = (metadata: ProfileMetadata) => { + if (!isUsersOwnProfile) { + return metadata.picture || getRoboHashPicture(npub!) + } + + // userRobotImage is used only when visiting own profile + // while kind 0 picture is not set + return metadata.picture || userRobotImage || getRoboHashPicture(npub!) + } + + return ( + <> + {isLoading && } +
+ + Profile Settings + + } + > + {profileMetadata && ( +
+ + { + event.target.src = getRoboHashPicture(npub!) + }} + className={styles.img} + src={getProfileImage(profileMetadata)} + alt="Profile Image" + /> + + {nostrJoiningBlock && ( + + On nostr since {nostrJoiningBlock.block.toLocaleString()} + + )} + + + {editItem('picture', 'Picture URL', undefined, undefined, { + endAdornment: isUsersOwnProfile ? robohashButton() : undefined + })} + + {editItem('name', 'Username')} + {editItem('display_name', 'Display Name')} + {editItem('nip05', 'Nostr Address (nip05)')} + {editItem('lud16', 'Lightning Address (lud16)')} + {editItem('about', 'About', true, 4)} + {editItem('website', 'Website')} + {isUsersOwnProfile && ( + <> + {usersPubkey && + copyItem(nip19.npubEncode(usersPubkey), 'Public Key')} + {loginMethod === LoginMethods.privateKey && + keys && + keys.private && + copyItem( + '••••••••••••••••••••••••••••••••••••••••••••••••••', + 'Private Key', + keys.private + )} + + )} +
+ )} +
+ {isUsersOwnProfile && ( + + SAVE + + )} +
+ + ) +} diff --git a/src/pages/settings/profile/style.module.scss b/src/pages/settings/profile/style.module.scss new file mode 100644 index 0000000..d5d3282 --- /dev/null +++ b/src/pages/settings/profile/style.module.scss @@ -0,0 +1,23 @@ +.container { + display: flex; + flex-direction: column; + gap: 25px; +} + +.textField { + width: 100%; +} + +.subHeader { + border-bottom: 0.5px solid; +} + +.img { + max-height: 40%; + max-width: 40%; +} + +.copyItem { + margin-left: 10px; + color: #34495e; +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 1796e19..a88732f 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -6,12 +6,14 @@ import { ProfilePage } from '../pages/profile' import { hexToNpub } from '../utils' import { SignPage } from '../pages/sign' import { VerifyPage } from '../pages/verify' +import { ProfileSettingsPage } from '../pages/settings/profile' export const appPrivateRoutes = { homePage: '/', create: '/create', sign: '/sign', - verify: '/verify' + verify: '/verify', + profileSettings: '/settings/profile/:npub' } export const appPublicRoutes = { @@ -24,6 +26,9 @@ export const appPublicRoutes = { export const getProfileRoute = (hexKey: string) => appPublicRoutes.profile.replace(':npub', hexToNpub(hexKey)) +export const getProfileSettingsRoute = (hexKey: string) => + appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey)) + export const publicRoutes = [ { path: appPublicRoutes.landingPage, @@ -57,5 +62,9 @@ export const privateRoutes = [ { path: appPrivateRoutes.verify, element: + }, + { + path: appPrivateRoutes.profileSettings, + element: } ] From 6eedfb8f3fe0785a98ed36408061c9ed7ab9645a Mon Sep 17 00:00:00 2001 From: Davinci Date: Tue, 21 May 2024 15:21:07 +0200 Subject: [PATCH 2/4] feat: added profile banner --- src/index.css | 2 +- src/pages/profile/index.tsx | 188 +++++++++---------- src/pages/profile/style.module.scss | 32 +++- src/pages/settings/profile/index.tsx | 19 ++ src/pages/settings/profile/style.module.scss | 14 ++ src/types/profile.ts | 1 + src/utils/string.ts | 7 + 7 files changed, 162 insertions(+), 101 deletions(-) diff --git a/src/index.css b/src/index.css index 3f10537..a9ed198 100644 --- a/src/index.css +++ b/src/index.css @@ -71,7 +71,7 @@ button { } .main { - padding: 64px 0; + padding: 60px 0; } .hide-mobile { diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index a7da30f..47087d1 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -16,12 +16,11 @@ import { NostrJoiningBlock, ProfileMetadata } from '../../types' import styles from './style.module.scss' import { useSelector } from 'react-redux' import { State } from '../../store/rootReducer' -import { getRoboHashPicture, shorten } from '../../utils' +import { getRoboHashPicture, hexToNpub, shorten } from '../../utils' import { truncate } from 'lodash' import { getProfileSettingsRoute } from '../../routes' import EditIcon from '@mui/icons-material/Edit'; import LinkIcon from '@mui/icons-material/Link'; -import RestoreIcon from '@mui/icons-material/Restore'; import { LoadingSpinner } from '../../components/LoadingSpinner' export const ProfilePage = () => { @@ -107,7 +106,13 @@ export const ProfilePage = () => { } }, [isUsersOwnProfile, metadataState, pubkey, metadataController]) - const textElementWithCopyIcon = (text: string) => { + /** + * Rendering text with button which copies the provided text + * @param text to be visible + * @param sx props (MUI) to customize style + * @returns HTML rendered text + */ + const textElementWithCopyIcon = (text: string, sx?: SxProps, shortenOffset?: number) => { const onClick = () => { navigator.clipboard.writeText(text) @@ -118,19 +123,13 @@ export const ProfilePage = () => { } return ( - - {shorten(text)} + + {shorten(text, shortenOffset)} ) } - const titleElement = (text: string, sx?: SxProps) => ( - - {text} - - ) - /** * Handles the logic for Image URL. * If no picture in kind 0 found - use robohash avatar @@ -154,97 +153,94 @@ export const ProfilePage = () => { {isLoading && } {pubkey && ( - - -
- -
-
- - - - - {profileMetadata && - titleElement( - truncate( - profileMetadata.display_name || - profileMetadata.name || - pubkey, - { - length: 16 - } - ), - { marginRight: 1 } - )} - - - {textElementWithCopyIcon(pubkey || '')} - - - {profileMetadata?.nip05 && - textElementWithCopyIcon(profileMetadata.nip05)} - - + + {profileMetadata && profileMetadata.banner ? : ''} + - - {profileMetadata?.website && ( - - - {profileMetadata.website} - - )} - - - - {nostrJoiningBlock && ( - - - On nostr since {nostrJoiningBlock.block.toLocaleString()} - - )} - + + + +
+ +
+
+ + + {nostrJoiningBlock ? `On nostr since ${nostrJoiningBlock.block.toLocaleString()}` : 'On nostr since: unknown'} + + + + {isUsersOwnProfile && ( + navigate(getProfileSettingsRoute(pubkey))}> + + + )}
- - {isUsersOwnProfile && ( - navigate(getProfileSettingsRoute(pubkey))}> - - + + + + {profileMetadata && + + {truncate( + profileMetadata.display_name || + profileMetadata.name || + hexToNpub(pubkey), + { + length: 16 + } + )} + } + + + {textElementWithCopyIcon(hexToNpub(pubkey) || '', { color: '#5e5e5e' }, 5)} + + + {profileMetadata?.nip05 && + textElementWithCopyIcon(profileMetadata.nip05, { color: '#5e5e5e' })} + + + {profileMetadata?.lud16 && + textElementWithCopyIcon(profileMetadata.lud16, { color: '#5e5e5e' })} + + + + + {profileMetadata?.website && ( + + {profileMetadata.website} + + + )} + + + + {profileMetadata?.about && ( + + {profileMetadata.about} + )} - - {profileMetadata?.about && ( - - {profileMetadata.about} - - )} -
)} diff --git a/src/pages/profile/style.module.scss b/src/pages/profile/style.module.scss index 3e1afcc..9fe952d 100644 --- a/src/pages/profile/style.module.scss +++ b/src/pages/profile/style.module.scss @@ -1,6 +1,26 @@ -.upper { +.banner { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + min-height: 210px; + + img { + width: 100%; + } + + &.noImage { + background-color: rgb(219, 219, 219); + } +} + +.belowBanner { + padding: 0 15px; +} + +.upper { + width: 100%; display: flex; - padding-top: 15px; } .container { @@ -9,6 +29,11 @@ .left { margin-right: 10px; + margin-top: -35px; +} + +.middle { + flex: 1; } .right { @@ -35,7 +60,6 @@ } .website { - // margin-top: 10px !important; margin-bottom: 15px 0 !important; } @@ -46,6 +70,6 @@ .captionIcon { color: #15999b; - margin-right: 10px; + margin-left: 5px; font-size: 12px; } \ No newline at end of file diff --git a/src/pages/settings/profile/index.tsx b/src/pages/settings/profile/index.tsx index 86799ed..7bde460 100644 --- a/src/pages/settings/profile/index.tsx +++ b/src/pages/settings/profile/index.tsx @@ -1,5 +1,6 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy' import { + Box, IconButton, InputProps, List, @@ -283,6 +284,24 @@ export const ProfileSettingsPage = () => { > {profileMetadata && (
+ + {profileMetadata.banner ? ( + Banner Image + ): No banner found } + + + {editItem('banner', 'Banner URL', undefined, undefined)} + { // return original string if it is not long enough if (str.length < offset * 2 + 4) return str From 2c60e2962fd096ad2675bb6d31e65473c90120f0 Mon Sep 17 00:00:00 2001 From: Davinci Date: Tue, 21 May 2024 15:21:21 +0200 Subject: [PATCH 3/4] style: lint --- src/pages/profile/index.tsx | 138 +++++++++++++++++---------- src/pages/settings/profile/index.tsx | 4 +- 2 files changed, 91 insertions(+), 51 deletions(-) diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 47087d1..14ddbb6 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -19,8 +19,8 @@ import { State } from '../../store/rootReducer' import { getRoboHashPicture, hexToNpub, shorten } from '../../utils' import { truncate } from 'lodash' import { getProfileSettingsRoute } from '../../routes' -import EditIcon from '@mui/icons-material/Edit'; -import LinkIcon from '@mui/icons-material/Link'; +import EditIcon from '@mui/icons-material/Edit' +import LinkIcon from '@mui/icons-material/Link' import { LoadingSpinner } from '../../components/LoadingSpinner' export const ProfilePage = () => { @@ -112,7 +112,11 @@ export const ProfilePage = () => { * @param sx props (MUI) to customize style * @returns HTML rendered text */ - const textElementWithCopyIcon = (text: string, sx?: SxProps, shortenOffset?: number) => { + const textElementWithCopyIcon = ( + text: string, + sx?: SxProps, + shortenOffset?: number + ) => { const onClick = () => { navigator.clipboard.writeText(text) @@ -153,8 +157,14 @@ export const ProfilePage = () => { {isLoading && } {pubkey && ( - - {profileMetadata && profileMetadata.banner ? : ''} + + {profileMetadata && profileMetadata.banner ? ( + + ) : ( + '' + )} @@ -175,64 +185,92 @@ export const ProfilePage = () => {
- - {nostrJoiningBlock ? `On nostr since ${nostrJoiningBlock.block.toLocaleString()}` : 'On nostr since: unknown'} + target="_blank" + sx={{ color: '#7b7b7b', marginTop: '15px', display: 'block' }} + variant="caption" + > + {nostrJoiningBlock + ? `On nostr since ${nostrJoiningBlock.block.toLocaleString()}` + : 'On nostr since: unknown'} {isUsersOwnProfile && ( - navigate(getProfileSettingsRoute(pubkey))}> - + navigate(getProfileSettingsRoute(pubkey))} + > + )}
- - + + {profileMetadata && ( + - {profileMetadata && - - {truncate( - profileMetadata.display_name || - profileMetadata.name || - hexToNpub(pubkey), - { - length: 16 - } - )} - } - - - {textElementWithCopyIcon(hexToNpub(pubkey) || '', { color: '#5e5e5e' }, 5)} - - - {profileMetadata?.nip05 && - textElementWithCopyIcon(profileMetadata.nip05, { color: '#5e5e5e' })} - - - {profileMetadata?.lud16 && - textElementWithCopyIcon(profileMetadata.lud16, { color: '#5e5e5e' })} - - - - - {profileMetadata?.website && ( - - {profileMetadata.website} - - - )} - + {truncate( + profileMetadata.display_name || + profileMetadata.name || + hexToNpub(pubkey), + { + length: 16 + } + )} +
+ )} + + {textElementWithCopyIcon( + hexToNpub(pubkey) || '', + { color: '#5e5e5e' }, + 5 + )} + + + {profileMetadata?.nip05 && + textElementWithCopyIcon(profileMetadata.nip05, { + color: '#5e5e5e' + })} + + + {profileMetadata?.lud16 && + textElementWithCopyIcon(profileMetadata.lud16, { + color: '#5e5e5e' + })} + + + + + {profileMetadata?.website && ( + + + {profileMetadata.website} + + + + )} + + {profileMetadata?.about && ( diff --git a/src/pages/settings/profile/index.tsx b/src/pages/settings/profile/index.tsx index 7bde460..9b68872 100644 --- a/src/pages/settings/profile/index.tsx +++ b/src/pages/settings/profile/index.tsx @@ -297,7 +297,9 @@ export const ProfileSettingsPage = () => { src={profileMetadata.banner} alt="Banner Image" /> - ): No banner found } + ) : ( + No banner found + )} {editItem('banner', 'Banner URL', undefined, undefined)} From 67e5c19870bdd66ac1fc2b4dd3231a2fb77831a7 Mon Sep 17 00:00:00 2001 From: Davinci Date: Wed, 22 May 2024 11:09:41 +0200 Subject: [PATCH 4/4] fix: profile page styling --- src/pages/profile/index.tsx | 32 ++++++++++---------------- src/pages/profile/style.module.scss | 35 +++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 14ddbb6..caa5387 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -189,7 +189,7 @@ export const ProfilePage = () => { component={Link} to={`https://njump.me/${nostrJoiningBlock?.encodedEventPointer || ''}`} target="_blank" - sx={{ color: '#7b7b7b', marginTop: '15px', display: 'block' }} + className={`${styles.nostrSince} ${styles.link}`} variant="caption" > {nostrJoiningBlock @@ -208,7 +208,7 @@ export const ProfilePage = () => { - + { > {profileMetadata && ( @@ -234,21 +234,17 @@ export const ProfilePage = () => { {textElementWithCopyIcon( hexToNpub(pubkey) || '', - { color: '#5e5e5e' }, - 5 + undefined, + 15 )} {profileMetadata?.nip05 && - textElementWithCopyIcon(profileMetadata.nip05, { - color: '#5e5e5e' - })} + textElementWithCopyIcon(profileMetadata.nip05, undefined, 15)} {profileMetadata?.lud16 && - textElementWithCopyIcon(profileMetadata.lud16, { - color: '#5e5e5e' - })} + textElementWithCopyIcon(profileMetadata.lud16, undefined, 15)} @@ -257,16 +253,12 @@ export const ProfilePage = () => { - - {profileMetadata.website} - - + {profileMetadata.website} )} diff --git a/src/pages/profile/style.module.scss b/src/pages/profile/style.module.scss index 9fe952d..c3c2034 100644 --- a/src/pages/profile/style.module.scss +++ b/src/pages/profile/style.module.scss @@ -54,13 +54,27 @@ width: 150px; } -.copyIcon { - font-size: 1.1rem !important; - margin-left: 5px; +.link { + &:hover { + color: #3a3a3a; + } +} + +.nostrSince { + color: #5c5c5c; + margin-top: 15px !important; + display: inline-block; + + &:hover { + color: #3a3a3a; + } } .website { - margin-bottom: 15px 0 !important; + margin-top: 8px !important; + text-decoration: underline; + text-decoration-color: #3e3e3e; + color: #3e3e3e; } .captionWrapper { @@ -72,4 +86,17 @@ color: #15999b; margin-left: 5px; font-size: 12px; +} + +.npubNipItem { + display: inline-flex; + justify-content: space-between; + color: #3e3e3e; + line-height: 1.3; + + .copyIcon { + font-size: 0.9rem !important; + margin-left: 5px; + margin-top: 2px; + } } \ No newline at end of file