feat: added profile view #64
@ -71,7 +71,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
padding: 64px 0;
|
padding: 60px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-mobile {
|
.hide-mobile {
|
||||||
|
@ -16,12 +16,11 @@ import { NostrJoiningBlock, ProfileMetadata } from '../../types'
|
|||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { State } from '../../store/rootReducer'
|
import { State } from '../../store/rootReducer'
|
||||||
import { getRoboHashPicture, shorten } from '../../utils'
|
import { getRoboHashPicture, hexToNpub, shorten } from '../../utils'
|
||||||
import { truncate } from 'lodash'
|
import { truncate } from 'lodash'
|
||||||
import { getProfileSettingsRoute } from '../../routes'
|
import { getProfileSettingsRoute } from '../../routes'
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import LinkIcon from '@mui/icons-material/Link';
|
import LinkIcon from '@mui/icons-material/Link';
|
||||||
import RestoreIcon from '@mui/icons-material/Restore';
|
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
|
|
||||||
export const ProfilePage = () => {
|
export const ProfilePage = () => {
|
||||||
@ -107,7 +106,13 @@ export const ProfilePage = () => {
|
|||||||
}
|
}
|
||||||
}, [isUsersOwnProfile, metadataState, pubkey, metadataController])
|
}, [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<Theme>, shortenOffset?: number) => {
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
navigator.clipboard.writeText(text)
|
navigator.clipboard.writeText(text)
|
||||||
|
|
||||||
@ -118,19 +123,13 @@ export const ProfilePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography variant="caption" className={styles.npubNipItem}>
|
<Typography variant="caption" sx={sx} className={styles.npubNipItem}>
|
||||||
<span onClick={onClick}>{shorten(text)}</span>
|
<span onClick={onClick}>{shorten(text, shortenOffset)}</span>
|
||||||
<ContentCopyIcon onClick={onClick} className={styles.copyIcon} />
|
<ContentCopyIcon onClick={onClick} className={styles.copyIcon} />
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const titleElement = (text: string, sx?: SxProps<Theme>) => (
|
|
||||||
<Typography sx={sx} variant="h6" className={styles.bold}>
|
|
||||||
{text}
|
|
||||||
</Typography>
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the logic for Image URL.
|
* Handles the logic for Image URL.
|
||||||
* If no picture in kind 0 found - use robohash avatar
|
* If no picture in kind 0 found - use robohash avatar
|
||||||
@ -154,6 +153,11 @@ export const ProfilePage = () => {
|
|||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
{pubkey && (
|
{pubkey && (
|
||||||
<Box className={styles.container}>
|
<Box className={styles.container}>
|
||||||
|
<Box className={`${styles.banner} ${!profileMetadata || !profileMetadata.banner ? styles.noImage : ''}`}>
|
||||||
|
{profileMetadata && profileMetadata.banner ? <img src={profileMetadata.banner}/> : ''}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box className={styles.belowBanner}>
|
||||||
<Box className={styles.upper}>
|
<Box className={styles.upper}>
|
||||||
<Box className={styles.left}>
|
<Box className={styles.left}>
|
||||||
<div
|
<div
|
||||||
@ -170,7 +174,24 @@ export const ProfilePage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box className={styles.middle}>
|
||||||
|
<Typography
|
||||||
|
component={Link}
|
||||||
|
to={`https://njump.me/${nostrJoiningBlock?.encodedEventPointer || ''}`}
|
||||||
|
target="_blank"
|
||||||
|
sx={{ color: '#7b7b7b', marginTop: '15px', display: 'block' }}
|
||||||
|
variant='caption'>
|
||||||
|
{nostrJoiningBlock ? `On nostr since ${nostrJoiningBlock.block.toLocaleString()}` : 'On nostr since: unknown'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box className={styles.right}>
|
||||||
|
{isUsersOwnProfile && (
|
||||||
|
<IconButton onClick={() => navigate(getProfileSettingsRoute(pubkey))}>
|
||||||
|
<EditIcon/>
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
<Box className={styles.head}>
|
<Box className={styles.head}>
|
||||||
<Box className={styles.npubNip}>
|
<Box className={styles.npubNip}>
|
||||||
<Box
|
<Box
|
||||||
@ -179,64 +200,38 @@ export const ProfilePage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{profileMetadata &&
|
{profileMetadata &&
|
||||||
titleElement(
|
<Typography sx={{ marginRight: 1 }} variant="h6" className={styles.bold}>
|
||||||
truncate(
|
{truncate(
|
||||||
profileMetadata.display_name ||
|
profileMetadata.display_name ||
|
||||||
profileMetadata.name ||
|
profileMetadata.name ||
|
||||||
pubkey,
|
hexToNpub(pubkey),
|
||||||
{
|
{
|
||||||
length: 16
|
length: 16
|
||||||
}
|
}
|
||||||
),
|
|
||||||
{ marginRight: 1 }
|
|
||||||
)}
|
)}
|
||||||
|
</Typography>}
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
{textElementWithCopyIcon(pubkey || '')}
|
{textElementWithCopyIcon(hexToNpub(pubkey) || '', { color: '#5e5e5e' }, 5)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
{profileMetadata?.nip05 &&
|
{profileMetadata?.nip05 &&
|
||||||
textElementWithCopyIcon(profileMetadata.nip05)}
|
textElementWithCopyIcon(profileMetadata.nip05, { color: '#5e5e5e' })}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
{profileMetadata?.lud16 &&
|
||||||
|
textElementWithCopyIcon(profileMetadata.lud16, { color: '#5e5e5e' })}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
{profileMetadata?.website && (
|
{profileMetadata?.website && (
|
||||||
<Typography sx={{ marginTop: '10px' }} variant='caption' className={`${styles.website} ${styles.captionWrapper}`}>
|
<Typography sx={{ marginTop: '10px' }} variant='caption' className={`${styles.website} ${styles.captionWrapper}`}>
|
||||||
|
<Typography variant="caption" sx={{color: '#5e5e5e'}} className={styles.npubNipItem}>{profileMetadata.website}</Typography>
|
||||||
<LinkIcon className={styles.captionIcon}/>
|
<LinkIcon className={styles.captionIcon}/>
|
||||||
{profileMetadata.website}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box>
|
|
||||||
{nostrJoiningBlock && (
|
|
||||||
<Typography
|
|
||||||
variant='caption'
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.getContrastText(
|
|
||||||
theme.palette.background.paper
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
className={styles.captionWrapper}
|
|
||||||
component={Link}
|
|
||||||
to={`https://njump.me/${nostrJoiningBlock.encodedEventPointer}`}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<RestoreIcon className={styles.captionIcon}/>
|
|
||||||
On nostr since {nostrJoiningBlock.block.toLocaleString()}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box className={styles.right}>
|
|
||||||
{isUsersOwnProfile && (
|
|
||||||
<IconButton onClick={() => navigate(getProfileSettingsRoute(pubkey))}>
|
|
||||||
<EditIcon/>
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
{profileMetadata?.about && (
|
{profileMetadata?.about && (
|
||||||
@ -246,6 +241,7 @@ export const ProfilePage = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -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;
|
display: flex;
|
||||||
padding-top: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@ -9,6 +29,11 @@
|
|||||||
|
|
||||||
.left {
|
.left {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
margin-top: -35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle {
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
@ -35,7 +60,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.website {
|
.website {
|
||||||
// margin-top: 10px !important;
|
|
||||||
margin-bottom: 15px 0 !important;
|
margin-bottom: 15px 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +70,6 @@
|
|||||||
|
|
||||||
.captionIcon {
|
.captionIcon {
|
||||||
color: #15999b;
|
color: #15999b;
|
||||||
margin-right: 10px;
|
margin-left: 5px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
IconButton,
|
IconButton,
|
||||||
InputProps,
|
InputProps,
|
||||||
List,
|
List,
|
||||||
@ -283,6 +284,24 @@ export const ProfileSettingsPage = () => {
|
|||||||
>
|
>
|
||||||
{profileMetadata && (
|
{profileMetadata && (
|
||||||
<div>
|
<div>
|
||||||
|
<ListItem
|
||||||
|
sx={{
|
||||||
|
marginTop: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{profileMetadata.banner ? (
|
||||||
|
<img
|
||||||
|
className={styles.bannerImg}
|
||||||
|
src={profileMetadata.banner}
|
||||||
|
alt="Banner Image"
|
||||||
|
/>
|
||||||
|
): <Box className={styles.noBanner}> No banner found </Box>}
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
{editItem('banner', 'Banner URL', undefined, undefined)}
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 1,
|
marginTop: 1,
|
||||||
|
@ -21,3 +21,17 @@
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
color: #34495e;
|
color: #34495e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noBanner {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
background: rgb(219, 219, 219);
|
||||||
|
color: rgb(88, 88, 88);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerImg {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -2,6 +2,7 @@ export interface ProfileMetadata {
|
|||||||
name?: string
|
name?: string
|
||||||
display_name?: string
|
display_name?: string
|
||||||
picture?: string
|
picture?: string
|
||||||
|
banner?: string
|
||||||
about?: string
|
about?: string
|
||||||
website?: string
|
website?: string
|
||||||
nip05?: string
|
nip05?: string
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Function will replace the middle of the string with 3 dots if length greater then
|
||||||
|
* offset value
|
||||||
|
* @param str string to shorten
|
||||||
|
* @param offset of how many chars to keep in the beginning and the end
|
||||||
|
* eg. 3 will keep first 3 chars and last 3 chars between the dots
|
||||||
|
*/
|
||||||
export const shorten = (str: string, offset = 9) => {
|
export const shorten = (str: string, offset = 9) => {
|
||||||
// return original string if it is not long enough
|
// return original string if it is not long enough
|
||||||
if (str.length < offset * 2 + 4) return str
|
if (str.length < offset * 2 + 4) return str
|
||||||
|
Loading…
Reference in New Issue
Block a user