New marks and settings refactor #323

Merged
enes merged 10 commits from fixes-7-3-25 into staging 2025-03-11 11:11:02 +00:00
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(
Review

I'll be good to add some explanatory comments

I'll be good to add some explanatory comments
mergeWithInitialValue(parsedStoredValue, initialValue)
)
Review

I'll be good to add some explanatory comments

I'll be good to add some explanatory comments
}
// 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()
const { usersPubkey, loginMethod } = useAppSelector((state) => state.auth)
const listItem = (label: string, disabled = false) => {
return ( return (
<> <NavLink to={to}>
<ListItemText {({ isActive }) => (
primary={label} <Button
fullWidth
sx={{ sx={{
color: theme.palette.text.primary transition: 'ease 0.3s',
}} justifyContent: 'start',
/> gap: '10px',
background: 'rgba(76,130,163,0)',
{!disabled && ( color: '#434343',
<ArrowForwardIosIcon fontWeight: 600,
style={{ opacity: 0.75,
color: theme.palette.action.active, textTransform: 'none',
marginRight: -10 ...(isActive
}} ? {
/> background: '#447592',
)} color: 'white'
</>
)
} }
: {}),
'&:hover': {
opacity: 0.85,
gap: '15px',
background: '#5e8eab',
color: 'white'
}
}}
variant={'text'}
>
{icon}
{label}
</Button>
)}
</NavLink>
)
}
export const SettingsLayout = () => {
const { loginMethod } = useAppSelector((state) => state.auth)
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',
paddingBottom: 2,
paddingTop: 2,
zIndex: 2
}}
>
Settings
</ListSubheader>
}
>
<ListItem component={Link} to={getProfileSettingsRoute(usersPubkey!)}>
<ListItemIcon>
<AccountCircleIcon />
</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> {Item(appPrivateRoutes.relays, <RouterIcon />, 'Relays')}
{loginMethod === LoginMethod.nostrLogin &&
Item(
appPrivateRoutes.nostrLogin,
<ExtensionIcon />,
'Nostr Login'
)}
</aside>
</div>
<div className={styles.content}>
<Outlet />
</div>
</div>
</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,26 +18,7 @@ export const NostrLoginPage = () => {
) )
return ( return (
<Container> <List>
<List
sx={{
width: '100%',
bgcolor: 'background.paper'
}}
subheader={
<ListSubheader
sx={{
fontSize: '1.5rem',
borderBottom: '0.5px solid',
paddingBottom: 2,
paddingTop: 2,
zIndex: 2
}}
>
Nostr Settings
</ListSubheader>
}
>
<ListItemButton <ListItemButton
onClick={() => { onClick={() => {
launchNostrLoginDialog('switch-account') launchNostrLoginDialog('switch-account')
@ -73,6 +52,5 @@ export const NostrLoginPage = () => {
</ListItemButton> </ListItemButton>
)} )}
</List> </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,38 +223,15 @@ 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
sx={{
bgcolor: 'background.paper',
marginTop: 2
}}
subheader={
<ListSubheader
sx={{
paddingBottom: 1,
paddingTop: 1,
fontSize: '1.5rem',
zIndex: 2
}}
className={styles.subHeader}
>
Profile Settings
</ListSubheader>
}
>
{userProfile && ( {userProfile && (
<div> <div>
<ListItem <ListItem
@ -307,7 +263,7 @@ export const ProfileSettingsPage = () => {
> >
<img <img
onError={(event: React.SyntheticEvent<HTMLImageElement>) => { onError={(event: React.SyntheticEvent<HTMLImageElement>) => {
event.currentTarget.src = getRoboHashPicture(npub!) event.currentTarget.src = getRoboHashPicture(usersPubkey!)
}} }}
className={styles.img} className={styles.img}
src={getProfileImage(userProfile)} src={getProfileImage(userProfile)}
@ -316,7 +272,7 @@ export const ProfileSettingsPage = () => {
</ListItem> </ListItem>
{editItem('image', 'Picture URL', undefined, undefined, { {editItem('image', 'Picture URL', undefined, undefined, {
endAdornment: isUsersOwnProfile ? robohashButton() : undefined endAdornment: robohashButton()
})} })}
{editItem('name', 'Username')} {editItem('name', 'Username')}
@ -325,11 +281,8 @@ export const ProfileSettingsPage = () => {
{editItem('lud16', 'Lightning Address (lud16)')} {editItem('lud16', 'Lightning Address (lud16)')}
{editItem('about', 'About', true, 4)} {editItem('about', 'About', true, 4)}
{editItem('website', 'Website')} {editItem('website', 'Website')}
{isUsersOwnProfile && (
<>
{usersPubkey && {usersPubkey &&
copyItem(nip19.npubEncode(usersPubkey), 'Public Key')} copyItem(nip19.npubEncode(usersPubkey), 'Public Key')}
{loginMethod === LoginMethod.privateKey && {loginMethod === LoginMethod.privateKey &&
keys && keys &&
keys.private && keys.private &&
@ -338,10 +291,6 @@ export const ProfileSettingsPage = () => {
'Private Key', 'Private Key',
keys.private keys.private
)} )}
</>
)}
{isUsersOwnProfile && (
<>
{loginMethod === LoginMethod.nostrLogin && {loginMethod === LoginMethod.nostrLogin &&
nostrLoginAuthMethod === NostrLoginAuthMethod.Local && ( nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
<ListItem <ListItem
@ -358,29 +307,22 @@ export const ProfileSettingsPage = () => {
disabled disabled
type={'password'} type={'password'}
InputProps={{ InputProps={{
endAdornment: ( endAdornment: <LaunchIcon className={styles.copyItem} />
<LaunchIcon className={styles.copyItem} />
)
}} }}
/> />
</ListItem> </ListItem>
)} )}
</>
)}
</div> </div>
)} )}
</List> </List>
{isUsersOwnProfile && (
<LoadingButton <LoadingButton
sx={{ maxWidth: '300px', alignSelf: 'center', width: '100%' }}
loading={savingProfileMetadata} loading={savingProfileMetadata}
variant="contained" variant="contained"
onClick={handleSaveMetadata} onClick={handleSaveMetadata}
> >
SAVE PUBLISH CHANGES
</LoadingButton> </LoadingButton>
)}
</Container>
<Footer />
</> </>
) )
} }

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,25 +1,22 @@
@import '../../../styles/colors.scss'; @import '../../../styles/colors.scss';
.container { .relayURItextfield {
color: $text-color;
.relayURItextfield {
width: 100%; width: 100%;
} }
.relayAddContainer { .relayAddContainer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 10px; gap: 10px;
width: 100%; width: 100%;
align-items: start; align-items: start;
} }
.sectionIcon { .sectionIcon {
font-size: 30px; font-size: 30px;
} }
.sectionTitle { .sectionTitle {
margin-top: 35px; margin-top: 35px;
margin-bottom: 10px; margin-bottom: 10px;
display: flex; display: flex;
@ -28,15 +25,15 @@
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
font-weight: 600; font-weight: 600;
} }
.relaysContainer { .relaysContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
} }
.relay { .relay {
border: 1px solid rgba(0, 0, 0, 0.12); border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px; border-radius: 4px;
@ -103,5 +100,4 @@
.connectionStatusUnknown { .connectionStatusUnknown {
background-color: $input-text-color; 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.cacheSettings,
element: (
<PrivateRoute>
<CacheSettingsPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.relays, path: appPrivateRoutes.relays,
element: ( element: <RelaysPage />
<PrivateRoute>
<RelaysPage />
</PrivateRoute>
)
}, },
{ {
path: appPrivateRoutes.nostrLogin, path: appPrivateRoutes.nostrLogin,
element: ( element: <NostrLoginPage />
<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,