chore(git): merge pull request #323 from fixes-7-3-25 into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m59s

Reviewed-on: #323
Reviewed-by: s <s@noreply.git.nostrdev.com>
This commit is contained in:
enes 2025-03-11 11:11:02 +00:00
commit ec9c4dad5d
25 changed files with 542 additions and 496 deletions

8
package-lock.json generated
View File

@ -24,7 +24,7 @@
"@nostr-dev-kit/ndk-cache-dexie": "2.5.9", "@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1", "@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4", "axios": "^1.8.2",
"crypto-hash": "3.0.0", "crypto-hash": "3.0.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dexie": "4.0.8", "dexie": "4.0.8",
@ -4051,9 +4051,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.4", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",

View File

@ -35,7 +35,7 @@
"@nostr-dev-kit/ndk-cache-dexie": "2.5.9", "@nostr-dev-kit/ndk-cache-dexie": "2.5.9",
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
"@reduxjs/toolkit": "2.2.1", "@reduxjs/toolkit": "2.2.1",
"axios": "^1.7.4", "axios": "^1.8.2",
"crypto-hash": "3.0.0", "crypto-hash": "3.0.0",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dexie": "4.0.8", "dexie": "4.0.8",

View File

@ -181,7 +181,7 @@ export const AppBar = () => {
onClick={() => { onClick={() => {
setAnchorElUser(null) setAnchorElUser(null)
navigate(appPrivateRoutes.settings) navigate(appPrivateRoutes.profileSettings)
}} }}
sx={{ sx={{
justifyContent: 'center' justifyContent: 'center'

View File

@ -0,0 +1,24 @@
import { MarkInputProps } from '../MarkStrategy'
import styles from '../../MarkFormField/style.module.scss'
import { useEffect, useRef } from 'react'
export const MarkInputDateTime = ({ handler, placeholder }: MarkInputProps) => {
const ref = useRef<HTMLInputElement>(null)
useEffect(() => {
if (ref.current) {
const date = new Date()
ref.current.value = date.toISOString().slice(0, 16)
handler(date.toUTCString())
}
}, [handler])
return (
<input
type="datetime-local"
ref={ref}
className={styles.input}
placeholder={placeholder}
readOnly={true}
disabled={true}
/>
)
}

View File

@ -0,0 +1,7 @@
import { MarkStrategy } from '../MarkStrategy'
import { MarkInputDateTime } from './Input'
export const DateTimeStrategy: MarkStrategy = {
input: MarkInputDateTime,
render: ({ value }) => <>{value}</>
}

View File

@ -0,0 +1,20 @@
import { useDidMount } from '../../../hooks'
import { useLocalStorage } from '../../../hooks/useLocalStorage'
import { MarkInputProps } from '../MarkStrategy'
import { MarkInputText } from '../Text/Input'
export const MarkInputFullName = (props: MarkInputProps) => {
const [fullName, setFullName] = useLocalStorage('mark-fullname', '')
useDidMount(() => {
props.handler(fullName)
})
return MarkInputText({
...props,
placeholder: 'Full Name',
value: fullName,
handler: (value) => {
setFullName(value)
props.handler(value)
}
})
}

View File

@ -0,0 +1,7 @@
import { MarkStrategy } from '../MarkStrategy'
import { MarkInputFullName } from './Input'
export const FullNameStrategy: MarkStrategy = {
input: MarkInputFullName,
render: ({ value }) => <>{value}</>
}

View File

@ -0,0 +1,20 @@
import { useDidMount } from '../../../hooks'
import { useLocalStorage } from '../../../hooks/useLocalStorage'
import { MarkInputProps } from '../MarkStrategy'
import { MarkInputText } from '../Text/Input'
export const MarkInputJobTitle = (props: MarkInputProps) => {
const [jobTitle, setjobTitle] = useLocalStorage('mark-jobtitle', '')
useDidMount(() => {
props.handler(jobTitle)
})
return MarkInputText({
...props,
placeholder: 'Job Title',
value: jobTitle,
handler: (value) => {
setjobTitle(value)
props.handler(value)
}
})
}

View File

@ -0,0 +1,7 @@
import { MarkStrategy } from '../MarkStrategy'
import { MarkInputJobTitle } from './Input'
export const JobTitleStrategy: MarkStrategy = {
input: MarkInputJobTitle,
render: ({ value }) => <>{value}</>
}

View File

@ -2,6 +2,9 @@ import { MarkType } from '../../types/drawing'
import { CurrentUserMark, Mark } from '../../types/mark' import { CurrentUserMark, Mark } from '../../types/mark'
import { TextStrategy } from './Text' import { TextStrategy } from './Text'
import { SignatureStrategy } from './Signature' import { SignatureStrategy } from './Signature'
import { FullNameStrategy } from './FullName'
import { JobTitleStrategy } from './JobTitle'
import { DateTimeStrategy } from './DateTime'
export interface MarkInputProps { export interface MarkInputProps {
value: string value: string
@ -28,5 +31,8 @@ export type MarkStrategies = {
export const MARK_TYPE_CONFIG: MarkStrategies = { export const MARK_TYPE_CONFIG: MarkStrategies = {
[MarkType.TEXT]: TextStrategy, [MarkType.TEXT]: TextStrategy,
[MarkType.SIGNATURE]: SignatureStrategy [MarkType.SIGNATURE]: SignatureStrategy,
[MarkType.FULLNAME]: FullNameStrategy,
[MarkType.JOBTITLE]: JobTitleStrategy,
[MarkType.DATETIME]: DateTimeStrategy
} }

View File

@ -8,6 +8,4 @@
position: absolute; position: absolute;
z-index: 40; z-index: 40;
display: flex; display: flex;
justify-content: center;
align-items: center;
} }

View File

@ -0,0 +1,72 @@
import React, { useMemo } from 'react'
import {
getLocalStorageItem,
mergeWithInitialValue,
removeLocalStorageItem,
setLocalStorageItem
} from '../utils'
/**
* Subscribe to the Browser's storage event. Get the new value if any of the tabs changes it.
* @param callback - function to be called when the storage event is triggered
* @returns clean up function
*/
const useLocalStorageSubscribe = (callback: () => void) => {
window.addEventListener('storage', callback)
return () => window.removeEventListener('storage', callback)
}
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] {
const getSnapshot = () => {
// Get the stored value
const storedValue = getLocalStorageItem(key, initialValue)
// Parse the value
const parsedStoredValue = JSON.parse(storedValue)
// Merge the default and the stored in case some of the required fields are missing
return JSON.stringify(
mergeWithInitialValue(parsedStoredValue, initialValue)
)
}
// https://react.dev/reference/react/useSyncExternalStore
// Returns the snapshot of the data and subscribes to the storage event
const data = React.useSyncExternalStore(useLocalStorageSubscribe, getSnapshot)
// Takes the value or a function that returns the value and updates the local storage
const setState: React.Dispatch<React.SetStateAction<T>> = React.useCallback(
(v: React.SetStateAction<T>) => {
try {
const nextState =
typeof v === 'function'
? (v as (prevState: T) => T)(JSON.parse(data))
: v
if (nextState === undefined || nextState === null) {
removeLocalStorageItem(key)
} else {
setLocalStorageItem(key, JSON.stringify(nextState))
}
} catch (e) {
console.warn(e)
}
},
[data, key]
)
React.useEffect(() => {
// Set local storage only when it's empty
const data = window.localStorage.getItem(key)
if (data === null) {
setLocalStorageItem(key, JSON.stringify(initialValue))
}
}, [key, initialValue])
const memoized = useMemo(() => JSON.parse(data) as T, [data])
return [memoized, setState]
}

View File

@ -13,7 +13,7 @@ import { Footer } from '../../components/Footer/Footer'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { useAppSelector } from '../../hooks/store' import { useAppSelector } from '../../hooks/store'
import { getProfileSettingsRoute } from '../../routes' import { appPrivateRoutes } from '../../routes'
import { import {
getProfileUsername, getProfileUsername,
@ -168,7 +168,7 @@ export const ProfilePage = () => {
<Box className={styles.right}> <Box className={styles.right}>
{isUsersOwnProfile && ( {isUsersOwnProfile && (
<IconButton <IconButton
onClick={() => navigate(getProfileSettingsRoute(pubkey))} onClick={() => navigate(appPrivateRoutes.profileSettings)}
> >
<EditIcon /> <EditIcon />
</IconButton> </IconButton>

View File

@ -1,94 +1,82 @@
import AccountCircleIcon from '@mui/icons-material/AccountCircle' import AccountCircleIcon from '@mui/icons-material/AccountCircle'
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'
import CachedIcon from '@mui/icons-material/Cached'
import RouterIcon from '@mui/icons-material/Router' import RouterIcon from '@mui/icons-material/Router'
import { ListItem, useTheme } from '@mui/material' import { Button } from '@mui/material'
import List from '@mui/material/List'
import ListItemIcon from '@mui/material/ListItemIcon'
import ListItemText from '@mui/material/ListItemText'
import ListSubheader from '@mui/material/ListSubheader'
import { useAppSelector } from '../../hooks/store' import { useAppSelector } from '../../hooks/store'
import { Link } from 'react-router-dom' import { NavLink, Outlet, To } from 'react-router-dom'
import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes' import { appPrivateRoutes } from '../../routes'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import { Footer } from '../../components/Footer/Footer' import { Footer } from '../../components/Footer/Footer'
import ExtensionIcon from '@mui/icons-material/Extension' import ExtensionIcon from '@mui/icons-material/Extension'
import { LoginMethod } from '../../store/auth/types' import { LoginMethod } from '../../store/auth/types'
import styles from './style.module.scss'
import { ReactNode } from 'react'
export const SettingsPage = () => { const Item = (to: To, icon: ReactNode, label: string) => {
const theme = useTheme() return (
const { usersPubkey, loginMethod } = useAppSelector((state) => state.auth) <NavLink to={to}>
const listItem = (label: string, disabled = false) => { {({ isActive }) => (
return ( <Button
<> fullWidth
<ListItemText
primary={label}
sx={{ sx={{
color: theme.palette.text.primary transition: 'ease 0.3s',
justifyContent: 'start',
gap: '10px',
background: 'rgba(76,130,163,0)',
color: '#434343',
fontWeight: 600,
opacity: 0.75,
textTransform: 'none',
...(isActive
? {
background: '#447592',
color: 'white'
}
: {}),
'&:hover': {
opacity: 0.85,
gap: '15px',
background: '#5e8eab',
color: 'white'
}
}} }}
/> variant={'text'}
>
{icon}
{label}
</Button>
)}
</NavLink>
)
}
{!disabled && ( export const SettingsLayout = () => {
<ArrowForwardIosIcon const { loginMethod } = useAppSelector((state) => state.auth)
style={{
color: theme.palette.action.active,
marginRight: -10
}}
/>
)}
</>
)
}
return ( return (
<> <>
<Container> <Container>
<List <h2 className={styles.title}>Settings</h2>
sx={{ <div className={styles.main}>
width: '100%', <div>
bgcolor: 'background.paper' <aside className={styles.aside}>
}} {Item(
subheader={ appPrivateRoutes.profileSettings,
<ListSubheader <AccountCircleIcon />,
sx={{ 'Profile'
fontSize: '1.5rem', )}
borderBottom: '0.5px solid', {Item(appPrivateRoutes.relays, <RouterIcon />, 'Relays')}
paddingBottom: 2, {loginMethod === LoginMethod.nostrLogin &&
paddingTop: 2, Item(
zIndex: 2 appPrivateRoutes.nostrLogin,
}} <ExtensionIcon />,
> 'Nostr Login'
Settings )}
</ListSubheader> </aside>
} </div>
> <div className={styles.content}>
<ListItem component={Link} to={getProfileSettingsRoute(usersPubkey!)}> <Outlet />
<ListItemIcon> </div>
<AccountCircleIcon /> </div>
</ListItemIcon>
{listItem('Profile')}
</ListItem>
<ListItem component={Link} to={appPrivateRoutes.relays}>
<ListItemIcon>
<RouterIcon />
</ListItemIcon>
{listItem('Relays')}
</ListItem>
<ListItem component={Link} to={appPrivateRoutes.cacheSettings}>
<ListItemIcon>
<CachedIcon />
</ListItemIcon>
{listItem('Local Cache')}
</ListItem>
{loginMethod === LoginMethod.nostrLogin && (
<ListItem component={Link} to={appPrivateRoutes.nostrLogin}>
<ListItemIcon>
<ExtensionIcon />
</ListItemIcon>
{listItem('Nostr Login')}
</ListItem>
)}
</List>
</Container> </Container>
<Footer /> <Footer />
</> </>

View File

@ -1,69 +0,0 @@
import InputIcon from '@mui/icons-material/Input'
import IosShareIcon from '@mui/icons-material/IosShare'
import {
List,
ListItemButton,
ListItemIcon,
ListItemText,
ListSubheader,
useTheme
} from '@mui/material'
import { Container } from '../../../components/Container'
import { Footer } from '../../../components/Footer/Footer'
export const CacheSettingsPage = () => {
const theme = useTheme()
const listItem = (label: string) => {
return (
<ListItemText
primary={label}
sx={{
color: theme.palette.text.primary
}}
/>
)
}
return (
<>
<Container>
<List
sx={{
width: '100%',
bgcolor: 'background.paper',
marginTop: 2
}}
subheader={
<ListSubheader
sx={{
fontSize: '1.5rem',
borderBottom: '0.5px solid',
paddingBottom: 2,
paddingTop: 2,
zIndex: 2
}}
>
Cache Setting
</ListSubheader>
}
>
<ListItemButton disabled>
<ListItemIcon>
<IosShareIcon />
</ListItemIcon>
{listItem('Export (coming soon)')}
</ListItemButton>
<ListItemButton disabled>
<ListItemIcon>
<InputIcon />
</ListItemIcon>
{listItem('Import (coming soon)')}
</ListItemButton>
</List>
</Container>
<Footer />
</>
)
}

View File

@ -3,11 +3,9 @@ import {
ListItemButton, ListItemButton,
ListItemIcon, ListItemIcon,
ListItemText, ListItemText,
ListSubheader,
useTheme useTheme
} from '@mui/material' } from '@mui/material'
import { launch as launchNostrLoginDialog } from 'nostr-login' import { launch as launchNostrLoginDialog } from 'nostr-login'
import { Container } from '../../../components/Container'
import PeopleIcon from '@mui/icons-material/People' import PeopleIcon from '@mui/icons-material/People'
import ImportExportIcon from '@mui/icons-material/ImportExport' import ImportExportIcon from '@mui/icons-material/ImportExport'
import { useAppSelector } from '../../../hooks/store' import { useAppSelector } from '../../../hooks/store'
@ -20,59 +18,39 @@ export const NostrLoginPage = () => {
) )
return ( return (
<Container> <List>
<List <ListItemButton
sx={{ onClick={() => {
width: '100%', launchNostrLoginDialog('switch-account')
bgcolor: 'background.paper'
}} }}
subheader={
<ListSubheader
sx={{
fontSize: '1.5rem',
borderBottom: '0.5px solid',
paddingBottom: 2,
paddingTop: 2,
zIndex: 2
}}
>
Nostr Settings
</ListSubheader>
}
> >
<ListItemIcon>
<PeopleIcon />
</ListItemIcon>
<ListItemText
primary={'Nostr Login Accounts'}
sx={{
color: theme.palette.text.primary
}}
/>
</ListItemButton>
{nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
<ListItemButton <ListItemButton
onClick={() => { onClick={() => {
launchNostrLoginDialog('switch-account') launchNostrLoginDialog('import')
}} }}
> >
<ListItemIcon> <ListItemIcon>
<PeopleIcon /> <ImportExportIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
primary={'Nostr Login Accounts'} primary={'Import / Export Keys'}
sx={{ sx={{
color: theme.palette.text.primary color: theme.palette.text.primary
}} }}
/> />
</ListItemButton> </ListItemButton>
{nostrLoginAuthMethod === NostrLoginAuthMethod.Local && ( )}
<ListItemButton </List>
onClick={() => {
launchNostrLoginDialog('import')
}}
>
<ListItemIcon>
<ImportExportIcon />
</ListItemIcon>
<ListItemText
primary={'Import / Export Keys'}
sx={{
color: theme.palette.text.primary
}}
/>
</ListItemButton>
)}
</List>
</Container>
) )
} }

View File

@ -1,5 +1,4 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { SmartToy } from '@mui/icons-material' import { SmartToy } from '@mui/icons-material'
@ -12,7 +11,6 @@ import {
InputProps, InputProps,
List, List,
ListItem, ListItem,
ListSubheader,
TextField, TextField,
Tooltip Tooltip
} from '@mui/material' } from '@mui/material'
@ -28,8 +26,6 @@ import { useAppDispatch, useAppSelector } from '../../../hooks/store'
import { getRoboHashPicture, unixNow } from '../../../utils' import { getRoboHashPicture, unixNow } from '../../../utils'
import { Container } from '../../../components/Container'
import { Footer } from '../../../components/Footer/Footer'
import { LoadingSpinner } from '../../../components/LoadingSpinner' import { LoadingSpinner } from '../../../components/LoadingSpinner'
import { setUserProfile as updateUserProfile } from '../../../store/actions' import { setUserProfile as updateUserProfile } from '../../../store/actions'
@ -41,10 +37,8 @@ import styles from './style.module.scss'
export const ProfileSettingsPage = () => { export const ProfileSettingsPage = () => {
const dispatch: Dispatch = useAppDispatch() const dispatch: Dispatch = useAppDispatch()
const { npub } = useParams()
const { ndk, findMetadata, publish } = useNDKContext() const { ndk, findMetadata, publish } = useNDKContext()
const [pubkey, setPubkey] = useState<string>()
const [userProfile, setUserProfile] = useState<NDKUserProfile | null>(null) const [userProfile, setUserProfile] = useState<NDKUserProfile | null>(null)
const userRobotImage = useAppSelector((state) => state.user.robotImage) const userRobotImage = useAppSelector((state) => state.user.robotImage)
@ -55,27 +49,13 @@ export const ProfileSettingsPage = () => {
) )
const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const [savingProfileMetadata, setSavingProfileMetadata] = useState(false)
const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc] = useState('Fetching metadata') const [loadingSpinnerDesc] = useState('Fetching metadata')
const robotSet = useRef(1) const robotSet = useRef(1)
useEffect(() => { useEffect(() => {
if (npub) { if (usersPubkey && currentUserProfile) {
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 (isUsersOwnProfile && currentUserProfile) {
setUserProfile(currentUserProfile) setUserProfile(currentUserProfile)
setIsLoading(false) setIsLoading(false)
@ -83,8 +63,8 @@ export const ProfileSettingsPage = () => {
return return
} }
if (pubkey) { if (usersPubkey) {
findMetadata(pubkey) findMetadata(usersPubkey)
.then((profile) => { .then((profile) => {
setUserProfile(profile) setUserProfile(profile)
}) })
@ -95,7 +75,7 @@ export const ProfileSettingsPage = () => {
setIsLoading(false) setIsLoading(false)
}) })
} }
}, [ndk, isUsersOwnProfile, currentUserProfile, pubkey, findMetadata]) }, [ndk, currentUserProfile, findMetadata, usersPubkey])
const editItem = ( const editItem = (
key: keyof NDKUserProfile, key: keyof NDKUserProfile,
@ -113,7 +93,6 @@ export const ProfileSettingsPage = () => {
multiline={multiline} multiline={multiline}
rows={rows} rows={rows}
className={styles.textField} className={styles.textField}
disabled={!isUsersOwnProfile}
InputProps={inputProps} InputProps={inputProps}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target const { value } = event.target
@ -170,7 +149,7 @@ export const ProfileSettingsPage = () => {
content: serializedProfile, content: serializedProfile,
created_at: unixNow(), created_at: unixNow(),
kind: kinds.Metadata, kind: kinds.Metadata,
pubkey: pubkey!, pubkey: usersPubkey!,
tags: [] tags: []
} }
@ -215,7 +194,7 @@ export const ProfileSettingsPage = () => {
robotSet.current++ robotSet.current++
if (robotSet.current > 5) robotSet.current = 1 if (robotSet.current > 5) robotSet.current = 1
const robotAvatarLink = getRoboHashPicture(npub!, robotSet.current) const robotAvatarLink = getRoboHashPicture(usersPubkey!, robotSet.current)
setUserProfile((prev) => ({ setUserProfile((prev) => ({
...prev, ...prev,
@ -244,143 +223,106 @@ export const ProfileSettingsPage = () => {
* @returns robohash image url * @returns robohash image url
*/ */
const getProfileImage = (profile: NDKUserProfile) => { const getProfileImage = (profile: NDKUserProfile) => {
if (!isUsersOwnProfile) {
return profile.image || getRoboHashPicture(npub!)
}
// userRobotImage is used only when visiting own profile // userRobotImage is used only when visiting own profile
// while kind 0 picture is not set // while kind 0 picture is not set
return profile.image || userRobotImage || getRoboHashPicture(npub!) return profile.image || userRobotImage || getRoboHashPicture(usersPubkey!)
} }
return ( return (
<> <>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />} {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<Container className={styles.container}> <List>
<List {userProfile && (
sx={{ <div>
bgcolor: 'background.paper', <ListItem
marginTop: 2
}}
subheader={
<ListSubheader
sx={{ sx={{
paddingBottom: 1, marginTop: 1,
paddingTop: 1, display: 'flex',
fontSize: '1.5rem', flexDirection: 'column'
zIndex: 2
}} }}
className={styles.subHeader}
> >
Profile Settings {userProfile.banner ? (
</ListSubheader>
}
>
{userProfile && (
<div>
<ListItem
sx={{
marginTop: 1,
display: 'flex',
flexDirection: 'column'
}}
>
{userProfile.banner ? (
<img
className={styles.bannerImg}
src={userProfile.banner}
alt="Banner Image"
/>
) : (
<Box className={styles.noBanner}> No banner found </Box>
)}
</ListItem>
{editItem('banner', 'Banner URL', undefined, undefined)}
<ListItem
sx={{
marginTop: 1,
display: 'flex',
flexDirection: 'column'
}}
>
<img <img
onError={(event: React.SyntheticEvent<HTMLImageElement>) => { className={styles.bannerImg}
event.currentTarget.src = getRoboHashPicture(npub!) src={userProfile.banner}
}} alt="Banner Image"
className={styles.img}
src={getProfileImage(userProfile)}
alt="Profile Image"
/> />
</ListItem> ) : (
<Box className={styles.noBanner}> No banner found </Box>
{editItem('image', 'Picture URL', undefined, undefined, {
endAdornment: isUsersOwnProfile ? robohashButton() : undefined
})}
{editItem('name', 'Username')}
{editItem('displayName', '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 === LoginMethod.privateKey &&
keys &&
keys.private &&
copyItem(
'••••••••••••••••••••••••••••••••••••••••••••••••••',
'Private Key',
keys.private
)}
</>
)} )}
{isUsersOwnProfile && ( </ListItem>
<>
{loginMethod === LoginMethod.nostrLogin && {editItem('banner', 'Banner URL', undefined, undefined)}
nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
<ListItem <ListItem
sx={{ marginTop: 1 }} sx={{
onClick={() => { marginTop: 1,
launchNostrLoginDialog('import') display: 'flex',
}} flexDirection: 'column'
> }}
<TextField >
label="Private Key (nostr-login)" <img
defaultValue="••••••••••••••••••••••••••••••••••••••••••••••••••" onError={(event: React.SyntheticEvent<HTMLImageElement>) => {
size="small" event.currentTarget.src = getRoboHashPicture(usersPubkey!)
className={styles.textField} }}
disabled className={styles.img}
type={'password'} src={getProfileImage(userProfile)}
InputProps={{ alt="Profile Image"
endAdornment: ( />
<LaunchIcon className={styles.copyItem} /> </ListItem>
)
}} {editItem('image', 'Picture URL', undefined, undefined, {
/> endAdornment: robohashButton()
</ListItem> })}
)}
</> {editItem('name', 'Username')}
{editItem('displayName', 'Display Name')}
{editItem('nip05', 'Nostr Address (nip05)')}
{editItem('lud16', 'Lightning Address (lud16)')}
{editItem('about', 'About', true, 4)}
{editItem('website', 'Website')}
{usersPubkey &&
copyItem(nip19.npubEncode(usersPubkey), 'Public Key')}
{loginMethod === LoginMethod.privateKey &&
keys &&
keys.private &&
copyItem(
'••••••••••••••••••••••••••••••••••••••••••••••••••',
'Private Key',
keys.private
)} )}
</div> {loginMethod === LoginMethod.nostrLogin &&
)} nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
</List> <ListItem
{isUsersOwnProfile && ( sx={{ marginTop: 1 }}
<LoadingButton onClick={() => {
loading={savingProfileMetadata} launchNostrLoginDialog('import')
variant="contained" }}
onClick={handleSaveMetadata} >
> <TextField
SAVE label="Private Key (nostr-login)"
</LoadingButton> defaultValue="••••••••••••••••••••••••••••••••••••••••••••••••••"
size="small"
className={styles.textField}
disabled
type={'password'}
InputProps={{
endAdornment: <LaunchIcon className={styles.copyItem} />
}}
/>
</ListItem>
)}
</div>
)} )}
</Container> </List>
<Footer /> <LoadingButton
sx={{ maxWidth: '300px', alignSelf: 'center', width: '100%' }}
loading={savingProfileMetadata}
variant="contained"
onClick={handleSaveMetadata}
>
PUBLISH CHANGES
</LoadingButton>
</> </>
) )
} }

View File

@ -1,9 +1,3 @@
.container {
display: flex;
flex-direction: column;
gap: 25px;
}
.textField { .textField {
width: 100%; width: 100%;
} }

View File

@ -12,7 +12,6 @@ import ListItemText from '@mui/material/ListItemText'
import Switch from '@mui/material/Switch' import Switch from '@mui/material/Switch'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { Container } from '../../../components/Container'
import { import {
useAppDispatch, useAppDispatch,
useAppSelector, useAppSelector,
@ -32,7 +31,6 @@ import {
timeout timeout
} from '../../../utils' } from '../../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { Footer } from '../../../components/Footer/Footer'
import { import {
getRelayListForUser, getRelayListForUser,
NDKRelayList, NDKRelayList,
@ -246,7 +244,7 @@ export const RelaysPage = () => {
} }
return ( return (
<Container className={styles.container}> <>
<Box className={styles.relayAddContainer}> <Box className={styles.relayAddContainer}>
<TextField <TextField
label="Add new relay" label="Add new relay"
@ -291,8 +289,7 @@ export const RelaysPage = () => {
))} ))}
</Box> </Box>
)} )}
<Footer /> </>
</Container>
) )
} }

View File

@ -1,107 +1,103 @@
@import '../../../styles/colors.scss'; @import '../../../styles/colors.scss';
.container { .relayURItextfield {
color: $text-color; width: 100%;
}
.relayURItextfield { .relayAddContainer {
width: 100%; display: flex;
flex-direction: row;
gap: 10px;
width: 100%;
align-items: start;
}
.sectionIcon {
font-size: 30px;
}
.sectionTitle {
margin-top: 35px;
margin-bottom: 10px;
display: flex;
flex-direction: row;
gap: 5px;
font-size: 1.5rem;
line-height: 2rem;
font-weight: 600;
}
.relaysContainer {
display: flex;
flex-direction: column;
gap: 15px;
}
.relay {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
.relayDivider {
margin-left: 10px;
margin-right: 10px;
} }
.relayAddContainer { .leaveRelayContainer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 10px; gap: 10px;
width: 100%; cursor: pointer;
align-items: start;
} }
.sectionIcon { .showInfo {
font-size: 30px; cursor: pointer;
} }
.sectionTitle { .showInfoIcon {
margin-top: 35px; margin-right: 3px;
margin-bottom: 10px; margin-bottom: auto;
vertical-align: middle;
}
.relayInfoContainer {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
gap: 5px; gap: 5px;
font-size: 1.5rem; text-wrap: wrap;
line-height: 2rem; }
.relayInfoTitle {
font-weight: 600; font-weight: 600;
} }
.relaysContainer { .relayInfoSubTitle {
display: flex; font-weight: 500;
flex-direction: column;
gap: 15px;
} }
.relay { .copyItem {
border: 1px solid rgba(0, 0, 0, 0.12); margin-left: 10px;
border-radius: 4px; color: #34495e;
vertical-align: bottom;
cursor: pointer;
}
.relayDivider { .connectionStatus {
margin-left: 10px; border-radius: 9999px;
margin-right: 10px; width: 10px;
} height: 10px;
margin-right: 5px;
margin-top: 2px;
}
.leaveRelayContainer { .connectionStatusConnected {
display: flex; background-color: $relay-status-connected;
flex-direction: row; }
gap: 10px;
cursor: pointer;
}
.showInfo { .connectionStatusNotConnected {
cursor: pointer; background-color: $relay-status-notconnected;
} }
.showInfoIcon { .connectionStatusUnknown {
margin-right: 3px; background-color: $input-text-color;
margin-bottom: auto;
vertical-align: middle;
}
.relayInfoContainer {
display: flex;
flex-direction: column;
gap: 5px;
text-wrap: wrap;
}
.relayInfoTitle {
font-weight: 600;
}
.relayInfoSubTitle {
font-weight: 500;
}
.copyItem {
margin-left: 10px;
color: #34495e;
vertical-align: bottom;
cursor: pointer;
}
.connectionStatus {
border-radius: 9999px;
width: 10px;
height: 10px;
margin-right: 5px;
margin-top: 2px;
}
.connectionStatusConnected {
background-color: $relay-status-connected;
}
.connectionStatusNotConnected {
background-color: $relay-status-notconnected;
}
.connectionStatusUnknown {
background-color: $input-text-color;
}
} }
} }

View File

@ -0,0 +1,43 @@
.title {
margin: 0 0 15px 0;
}
.main {
width: 100%;
display: grid;
grid-template-columns: 0.4fr 1.6fr;
position: relative;
grid-gap: 25px;
>* {
width: 100%;
display: flex;
flex-direction: column;
grid-gap: 25px;
}
}
.aside {
width: 100%;
background: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
grid-gap: 15px;
position: sticky;
top: 15px;
}
.content {
width: 100%;
background: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 0 4px 0 rgb(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
grid-gap: 15px;
}

View File

@ -4,9 +4,7 @@ export const appPrivateRoutes = {
homePage: '/', homePage: '/',
create: '/create', create: '/create',
sign: '/sign', sign: '/sign',
settings: '/settings', profileSettings: '/settings/profile',
profileSettings: '/settings/profile/:npub',
cacheSettings: '/settings/cache',
relays: '/settings/relays', relays: '/settings/relays',
nostrLogin: '/settings/nostrLogin' nostrLogin: '/settings/nostrLogin'
} }
@ -24,6 +22,3 @@ export const appPublicRoutes = {
export const getProfileRoute = (hexKey: string) => export const getProfileRoute = (hexKey: string) =>
appPublicRoutes.profile.replace(':npub', hexToNpub(hexKey)) appPublicRoutes.profile.replace(':npub', hexToNpub(hexKey))
export const getProfileSettingsRoute = (hexKey: string) =>
appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey))

View File

@ -4,11 +4,10 @@ import { CreatePage } from '../pages/create'
import { HomePage } from '../pages/home' import { HomePage } from '../pages/home'
import { LandingPage } from '../pages/landing' import { LandingPage } from '../pages/landing'
import { ProfilePage } from '../pages/profile' import { ProfilePage } from '../pages/profile'
import { CacheSettingsPage } from '../pages/settings/cache'
import { NostrLoginPage } from '../pages/settings/nostrLogin' import { NostrLoginPage } from '../pages/settings/nostrLogin'
import { ProfileSettingsPage } from '../pages/settings/profile' import { ProfileSettingsPage } from '../pages/settings/profile'
import { RelaysPage } from '../pages/settings/relays' import { RelaysPage } from '../pages/settings/relays'
import { SettingsPage } from '../pages/settings/Settings' import { SettingsLayout } from '../pages/settings/Settings'
import { SignPage } from '../pages/sign' import { SignPage } from '../pages/sign'
import { VerifyPage } from '../pages/verify' import { VerifyPage } from '../pages/verify'
import { PrivateRoute } from './PrivateRoute' import { PrivateRoute } from './PrivateRoute'
@ -38,7 +37,7 @@ export function recursiveRouteRenderer<T>(
return routes.map((route, index) => return routes.map((route, index) =>
renderConditionCallbackFn(route) ? ( renderConditionCallbackFn(route) ? (
<Route <Route
key={`${route.path}${index}`} key={route.path ? `${route.path}${index}` : index}
path={route.path} path={route.path}
element={route.element} element={route.element}
> >
@ -68,7 +67,7 @@ export const publicRoutes: PublicRouteProps[] = [
} }
] ]
export const privateRoutes = [ export const privateRoutes: CustomRouteProps<unknown>[] = [
{ {
path: appPrivateRoutes.homePage, path: appPrivateRoutes.homePage,
element: ( element: (
@ -94,43 +93,24 @@ export const privateRoutes = [
) )
}, },
{ {
path: appPrivateRoutes.settings,
element: ( element: (
<PrivateRoute> <PrivateRoute>
<SettingsPage /> <SettingsLayout />
</PrivateRoute> </PrivateRoute>
) ),
}, children: [
{ {
path: appPrivateRoutes.profileSettings, path: appPrivateRoutes.profileSettings,
element: ( element: <ProfileSettingsPage />
<PrivateRoute> },
<ProfileSettingsPage /> {
</PrivateRoute> path: appPrivateRoutes.relays,
) element: <RelaysPage />
}, },
{ {
path: appPrivateRoutes.cacheSettings, path: appPrivateRoutes.nostrLogin,
element: ( element: <NostrLoginPage />
<PrivateRoute> }
<CacheSettingsPage /> ]
</PrivateRoute>
)
},
{
path: appPrivateRoutes.relays,
element: (
<PrivateRoute>
<RelaysPage />
</PrivateRoute>
)
},
{
path: appPrivateRoutes.nostrLogin,
element: (
<PrivateRoute>
<NostrLoginPage />
</PrivateRoute>
)
} }
] ]

View File

@ -42,3 +42,47 @@ export const clear = () => {
clearAuthToken() clearAuthToken()
clearState() clearState()
} }
export function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
if (
!Array.isArray(storedValue) &&
typeof storedValue === 'object' &&
storedValue !== null
) {
return { ...initialValue, ...storedValue }
}
return storedValue
}
export function getLocalStorageItem<T>(key: string, defaultValue: T): string {
try {
const data = window.localStorage.getItem(key)
if (data === null) return JSON.stringify(defaultValue)
return data
} catch (err) {
console.error(`Error while fetching local storage value: `, err)
return JSON.stringify(defaultValue)
}
}
export function setLocalStorageItem(key: string, value: string) {
try {
window.localStorage.setItem(key, value)
dispatchLocalStorageEvent(key, value)
} catch (err) {
console.error(`Error while saving local storage value: `, err)
}
}
export function removeLocalStorageItem(key: string) {
try {
window.localStorage.removeItem(key)
dispatchLocalStorageEvent(key, null)
} catch (err) {
console.error(`Error while deleting local storage value: `, err)
}
}
function dispatchLocalStorageEvent(key: string, newValue: string | null) {
window.dispatchEvent(new StorageEvent('storage', { key, newValue }))
}

View File

@ -171,20 +171,17 @@ export const DEFAULT_TOOLBOX: DrawTool[] = [
{ {
identifier: MarkType.FULLNAME, identifier: MarkType.FULLNAME,
icon: faIdCard, icon: faIdCard,
label: 'Full Name', label: 'Full Name'
isComingSoon: true
}, },
{ {
identifier: MarkType.JOBTITLE, identifier: MarkType.JOBTITLE,
icon: faBriefcase, icon: faBriefcase,
label: 'Job Title', label: 'Job Title'
isComingSoon: true
}, },
{ {
identifier: MarkType.DATETIME, identifier: MarkType.DATETIME,
icon: faClock, icon: faClock,
label: 'Date Time', label: 'Date Time'
isComingSoon: true
}, },
{ {
identifier: MarkType.NUMBER, identifier: MarkType.NUMBER,