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

View File

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

View File

@ -181,7 +181,7 @@ export const AppBar = () => {
onClick={() => {
setAnchorElUser(null)
navigate(appPrivateRoutes.settings)
navigate(appPrivateRoutes.profileSettings)
}}
sx={{
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 { TextStrategy } from './Text'
import { SignatureStrategy } from './Signature'
import { FullNameStrategy } from './FullName'
import { JobTitleStrategy } from './JobTitle'
import { DateTimeStrategy } from './DateTime'
export interface MarkInputProps {
value: string
@ -28,5 +31,8 @@ export type MarkStrategies = {
export const MARK_TYPE_CONFIG: MarkStrategies = {
[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;
z-index: 40;
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 { useAppSelector } from '../../hooks/store'
import { getProfileSettingsRoute } from '../../routes'
import { appPrivateRoutes } from '../../routes'
import {
getProfileUsername,
@ -168,7 +168,7 @@ export const ProfilePage = () => {
<Box className={styles.right}>
{isUsersOwnProfile && (
<IconButton
onClick={() => navigate(getProfileSettingsRoute(pubkey))}
onClick={() => navigate(appPrivateRoutes.profileSettings)}
>
<EditIcon />
</IconButton>

View File

@ -1,94 +1,82 @@
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 { ListItem, useTheme } 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 { Button } from '@mui/material'
import { useAppSelector } from '../../hooks/store'
import { Link } from 'react-router-dom'
import { appPrivateRoutes, getProfileSettingsRoute } from '../../routes'
import { NavLink, Outlet, To } from 'react-router-dom'
import { appPrivateRoutes } from '../../routes'
import { Container } from '../../components/Container'
import { Footer } from '../../components/Footer/Footer'
import ExtensionIcon from '@mui/icons-material/Extension'
import { LoginMethod } from '../../store/auth/types'
import styles from './style.module.scss'
import { ReactNode } from 'react'
export const SettingsPage = () => {
const theme = useTheme()
const { usersPubkey, loginMethod } = useAppSelector((state) => state.auth)
const listItem = (label: string, disabled = false) => {
return (
<>
<ListItemText
primary={label}
const Item = (to: To, icon: ReactNode, label: string) => {
return (
<NavLink to={to}>
{({ isActive }) => (
<Button
fullWidth
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 && (
<ArrowForwardIosIcon
style={{
color: theme.palette.action.active,
marginRight: -10
}}
/>
)}
</>
)
}
export const SettingsLayout = () => {
const { loginMethod } = useAppSelector((state) => state.auth)
return (
<>
<Container>
<List
sx={{
width: '100%',
bgcolor: 'background.paper'
}}
subheader={
<ListSubheader
sx={{
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>
<h2 className={styles.title}>Settings</h2>
<div className={styles.main}>
<div>
<aside className={styles.aside}>
{Item(
appPrivateRoutes.profileSettings,
<AccountCircleIcon />,
'Profile'
)}
{Item(appPrivateRoutes.relays, <RouterIcon />, 'Relays')}
{loginMethod === LoginMethod.nostrLogin &&
Item(
appPrivateRoutes.nostrLogin,
<ExtensionIcon />,
'Nostr Login'
)}
</aside>
</div>
<div className={styles.content}>
<Outlet />
</div>
</div>
</Container>
<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,
ListItemIcon,
ListItemText,
ListSubheader,
useTheme
} from '@mui/material'
import { launch as launchNostrLoginDialog } from 'nostr-login'
import { Container } from '../../../components/Container'
import PeopleIcon from '@mui/icons-material/People'
import ImportExportIcon from '@mui/icons-material/ImportExport'
import { useAppSelector } from '../../../hooks/store'
@ -20,59 +18,39 @@ export const NostrLoginPage = () => {
)
return (
<Container>
<List
sx={{
width: '100%',
bgcolor: 'background.paper'
<List>
<ListItemButton
onClick={() => {
launchNostrLoginDialog('switch-account')
}}
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
onClick={() => {
launchNostrLoginDialog('switch-account')
launchNostrLoginDialog('import')
}}
>
<ListItemIcon>
<PeopleIcon />
<ImportExportIcon />
</ListItemIcon>
<ListItemText
primary={'Nostr Login Accounts'}
primary={'Import / Export Keys'}
sx={{
color: theme.palette.text.primary
}}
/>
</ListItemButton>
{nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
<ListItemButton
onClick={() => {
launchNostrLoginDialog('import')
}}
>
<ListItemIcon>
<ImportExportIcon />
</ListItemIcon>
<ListItemText
primary={'Import / Export Keys'}
sx={{
color: theme.palette.text.primary
}}
/>
</ListItemButton>
)}
</List>
</Container>
)}
</List>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -42,3 +42,47 @@ export const clear = () => {
clearAuthToken()
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,
icon: faIdCard,
label: 'Full Name',
isComingSoon: true
label: 'Full Name'
},
{
identifier: MarkType.JOBTITLE,
icon: faBriefcase,
label: 'Job Title',
isComingSoon: true
label: 'Job Title'
},
{
identifier: MarkType.DATETIME,
icon: faClock,
label: 'Date Time',
isComingSoon: true
label: 'Date Time'
},
{
identifier: MarkType.NUMBER,