New marks and settings refactor #323
8
package-lock.json
generated
8
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -181,7 +181,7 @@ export const AppBar = () => {
|
||||
onClick={() => {
|
||||
setAnchorElUser(null)
|
||||
|
||||
navigate(appPrivateRoutes.settings)
|
||||
navigate(appPrivateRoutes.profileSettings)
|
||||
}}
|
||||
sx={{
|
||||
justifyContent: 'center'
|
||||
|
24
src/components/MarkTypeStrategy/DateTime/Input.tsx
Normal file
24
src/components/MarkTypeStrategy/DateTime/Input.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
7
src/components/MarkTypeStrategy/DateTime/index.tsx
Normal file
7
src/components/MarkTypeStrategy/DateTime/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { MarkStrategy } from '../MarkStrategy'
|
||||
import { MarkInputDateTime } from './Input'
|
||||
|
||||
export const DateTimeStrategy: MarkStrategy = {
|
||||
input: MarkInputDateTime,
|
||||
render: ({ value }) => <>{value}</>
|
||||
}
|
20
src/components/MarkTypeStrategy/FullName/Input.tsx
Normal file
20
src/components/MarkTypeStrategy/FullName/Input.tsx
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
7
src/components/MarkTypeStrategy/FullName/index.tsx
Normal file
7
src/components/MarkTypeStrategy/FullName/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { MarkStrategy } from '../MarkStrategy'
|
||||
import { MarkInputFullName } from './Input'
|
||||
|
||||
export const FullNameStrategy: MarkStrategy = {
|
||||
input: MarkInputFullName,
|
||||
render: ({ value }) => <>{value}</>
|
||||
}
|
20
src/components/MarkTypeStrategy/JobTitle/Input.tsx
Normal file
20
src/components/MarkTypeStrategy/JobTitle/Input.tsx
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
7
src/components/MarkTypeStrategy/JobTitle/index.tsx
Normal file
7
src/components/MarkTypeStrategy/JobTitle/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { MarkStrategy } from '../MarkStrategy'
|
||||
import { MarkInputJobTitle } from './Input'
|
||||
|
||||
export const JobTitleStrategy: MarkStrategy = {
|
||||
input: MarkInputJobTitle,
|
||||
render: ({ value }) => <>{value}</>
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -8,6 +8,4 @@
|
||||
position: absolute;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
72
src/hooks/useLocalStorage.ts
Normal file
72
src/hooks/useLocalStorage.ts
Normal 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)
|
||||
)
|
||||
s
commented
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]
|
||||
}
|
@ -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>
|
||||
|
@ -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) => {
|
||||
const Item = (to: To, icon: ReactNode, label: string) => {
|
||||
return (
|
||||
<>
|
||||
<ListItemText
|
||||
primary={label}
|
||||
<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'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{!disabled && (
|
||||
<ArrowForwardIosIcon
|
||||
style={{
|
||||
color: theme.palette.action.active,
|
||||
marginRight: -10
|
||||
}}
|
||||
/>
|
||||
variant={'text'}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
<h2 className={styles.title}>Settings</h2>
|
||||
<div className={styles.main}>
|
||||
<div>
|
||||
<aside className={styles.aside}>
|
||||
{Item(
|
||||
appPrivateRoutes.profileSettings,
|
||||
<AccountCircleIcon />,
|
||||
'Profile'
|
||||
)}
|
||||
</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>
|
||||
<Footer />
|
||||
</>
|
||||
|
69
src/pages/settings/cache/index.tsx
vendored
69
src/pages/settings/cache/index.tsx
vendored
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
@ -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,26 +18,7 @@ export const NostrLoginPage = () => {
|
||||
)
|
||||
|
||||
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
|
||||
}}
|
||||
>
|
||||
Nostr Settings
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<List>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
launchNostrLoginDialog('switch-account')
|
||||
@ -73,6 +52,5 @@ export const NostrLoginPage = () => {
|
||||
</ListItemButton>
|
||||
)}
|
||||
</List>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
@ -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,38 +223,15 @@ 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
|
||||
sx={{
|
||||
paddingBottom: 1,
|
||||
paddingTop: 1,
|
||||
fontSize: '1.5rem',
|
||||
zIndex: 2
|
||||
}}
|
||||
className={styles.subHeader}
|
||||
>
|
||||
Profile Settings
|
||||
</ListSubheader>
|
||||
}
|
||||
>
|
||||
<List>
|
||||
{userProfile && (
|
||||
<div>
|
||||
<ListItem
|
||||
@ -307,7 +263,7 @@ export const ProfileSettingsPage = () => {
|
||||
>
|
||||
<img
|
||||
onError={(event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
event.currentTarget.src = getRoboHashPicture(npub!)
|
||||
event.currentTarget.src = getRoboHashPicture(usersPubkey!)
|
||||
}}
|
||||
className={styles.img}
|
||||
src={getProfileImage(userProfile)}
|
||||
@ -316,7 +272,7 @@ export const ProfileSettingsPage = () => {
|
||||
</ListItem>
|
||||
|
||||
{editItem('image', 'Picture URL', undefined, undefined, {
|
||||
endAdornment: isUsersOwnProfile ? robohashButton() : undefined
|
||||
endAdornment: robohashButton()
|
||||
})}
|
||||
|
||||
{editItem('name', 'Username')}
|
||||
@ -325,11 +281,8 @@ export const ProfileSettingsPage = () => {
|
||||
{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 &&
|
||||
@ -338,10 +291,6 @@ export const ProfileSettingsPage = () => {
|
||||
'Private Key',
|
||||
keys.private
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isUsersOwnProfile && (
|
||||
<>
|
||||
{loginMethod === LoginMethod.nostrLogin &&
|
||||
nostrLoginAuthMethod === NostrLoginAuthMethod.Local && (
|
||||
<ListItem
|
||||
@ -358,29 +307,22 @@ export const ProfileSettingsPage = () => {
|
||||
disabled
|
||||
type={'password'}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<LaunchIcon className={styles.copyItem} />
|
||||
)
|
||||
endAdornment: <LaunchIcon className={styles.copyItem} />
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
{isUsersOwnProfile && (
|
||||
<LoadingButton
|
||||
sx={{ maxWidth: '300px', alignSelf: 'center', width: '100%' }}
|
||||
loading={savingProfileMetadata}
|
||||
variant="contained"
|
||||
onClick={handleSaveMetadata}
|
||||
>
|
||||
SAVE
|
||||
PUBLISH CHANGES
|
||||
</LoadingButton>
|
||||
)}
|
||||
</Container>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,9 +1,3 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.textField {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
@import '../../../styles/colors.scss';
|
||||
|
||||
.container {
|
||||
color: $text-color;
|
||||
|
||||
.relayURItextfield {
|
||||
width: 100%;
|
||||
}
|
||||
@ -104,4 +101,3 @@
|
||||
background-color: $input-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
43
src/pages/settings/style.module.scss
Normal file
43
src/pages/settings/style.module.scss
Normal 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;
|
||||
}
|
@ -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))
|
||||
|
@ -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>
|
||||
)
|
||||
},
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: appPrivateRoutes.profileSettings,
|
||||
element: (
|
||||
<PrivateRoute>
|
||||
<ProfileSettingsPage />
|
||||
</PrivateRoute>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.cacheSettings,
|
||||
element: (
|
||||
<PrivateRoute>
|
||||
<CacheSettingsPage />
|
||||
</PrivateRoute>
|
||||
)
|
||||
element: <ProfileSettingsPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.relays,
|
||||
element: (
|
||||
<PrivateRoute>
|
||||
<RelaysPage />
|
||||
</PrivateRoute>
|
||||
)
|
||||
element: <RelaysPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.nostrLogin,
|
||||
element: (
|
||||
<PrivateRoute>
|
||||
<NostrLoginPage />
|
||||
</PrivateRoute>
|
||||
)
|
||||
element: <NostrLoginPage />
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -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 }))
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user
I'll be good to add some explanatory comments