add close button in modals & add app details page

This commit is contained in:
Bekbolsun
2024-01-29 21:10:11 +06:00
parent 5fa22a2d9e
commit 3fa6e1cdaa
32 changed files with 474 additions and 75 deletions

32
package-lock.json generated
View File

@@ -23,7 +23,9 @@
"@types/react-copy-to-clipboard": "^5.0.7", "@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"date-fns": "^3.3.1",
"dexie": "^3.2.4", "dexie": "^3.2.4",
"dexie-react-hooks": "^1.1.7",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"nostr-tools": "^1.17.0", "nostr-tools": "^1.17.0",
@@ -7245,6 +7247,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/date-fns": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
"integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -7465,6 +7476,16 @@
"node": ">=6.0" "node": ">=6.0"
} }
}, },
"node_modules/dexie-react-hooks": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.7.tgz",
"integrity": "sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==",
"peerDependencies": {
"@types/react": ">=16",
"dexie": "^3.2 || ^4.0.1-alpha",
"react": ">=16"
}
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -23335,6 +23356,11 @@
"whatwg-url": "^8.0.0" "whatwg-url": "^8.0.0"
} }
}, },
"date-fns": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
"integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw=="
},
"debug": { "debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -23496,6 +23522,12 @@
"resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.4.tgz", "resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.4.tgz",
"integrity": "sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA==" "integrity": "sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA=="
}, },
"dexie-react-hooks": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.7.tgz",
"integrity": "sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==",
"requires": {}
},
"didyoumean": { "didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",

View File

@@ -18,7 +18,9 @@
"@types/react-copy-to-clipboard": "^5.0.7", "@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"date-fns": "^3.3.1",
"dexie": "^3.2.4", "dexie": "^3.2.4",
"dexie-react-hooks": "^1.1.7",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"nostr-tools": "^1.17.0", "nostr-tools": "^1.17.0",

View File

@@ -1,7 +1,7 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Modal } from '@/shared/Modal/Modal' import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { call, getShortenNpub } from '@/utils/helpers' import { call, getShortenNpub } from '@/utils/helpers/helpers'
import { Avatar, Box, Stack, Typography } from '@mui/material' import { Avatar, Box, Stack, Typography } from '@mui/material'
import { useParams, useSearchParams } from 'react-router-dom' import { useParams, useSearchParams } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks/redux' import { useAppSelector } from '@/store/hooks/redux'

View File

@@ -1,7 +1,7 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Modal } from '@/shared/Modal/Modal' import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { call, getShortenNpub } from '@/utils/helpers' import { call, getShortenNpub } from '@/utils/helpers/helpers'
import { import {
Avatar, Avatar,
Box, Box,
@@ -47,6 +47,7 @@ type ModalConfirmEventProps = {
export const ACTIONS: { [type: string]: string } = { export const ACTIONS: { [type: string]: string } = {
get_public_key: 'Get public key', get_public_key: 'Get public key',
sign_event: 'Sign event', sign_event: 'Sign event',
connect: 'Connect',
} }
type PendingRequest = DbPending & { checked: boolean } type PendingRequest = DbPending & { checked: boolean }

View File

@@ -6,7 +6,7 @@ import { Input } from '@/shared/Input/Input'
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton' import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
import { Modal } from '@/shared/Modal/Modal' import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { getBunkerLink } from '@/utils/helpers' import { getBunkerLink } from '@/utils/helpers/helpers'
import { Stack, Typography } from '@mui/material' import { Stack, Typography } from '@mui/material'
import { useRef } from 'react' import { useRef } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Button } from '@/shared/Button/Button' import { Button } from '@/shared/Button/Button'
import { Modal } from '@/shared/Modal/Modal' import { Modal } from '@/shared/Modal/Modal'
@@ -18,9 +18,17 @@ export const ModalInitial = () => {
setShowAdvancedContent(true) setShowAdvancedContent(true)
} }
useEffect(() => {
return () => {
if (isModalOpened) {
setShowAdvancedContent(false)
}
}
}, [isModalOpened])
return ( return (
<Modal open={isModalOpened} onClose={handleCloseModal}> <Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack paddingTop={'1rem'} gap={'1rem'}> <Stack paddingTop={'2.5rem'} gap={'1rem'}>
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}> <Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>
Sign up Sign up
</Button> </Button>

View File

@@ -11,6 +11,7 @@ import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button' import { Button } from '@/shared/Button/Button'
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined' import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined' import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
import { useNavigate } from 'react-router-dom'
export const ModalLogin = () => { export const ModalLogin = () => {
const { getModalOpened, handleClose } = useModalSearchParams() const { getModalOpened, handleClose } = useModalSearchParams()
@@ -19,6 +20,8 @@ export const ModalLogin = () => {
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const navigate = useNavigate()
const [enteredUsername, setEnteredUsername] = useState('') const [enteredUsername, setEnteredUsername] = useState('')
const [enteredPassword, setEnteredPassword] = useState('') const [enteredPassword, setEnteredPassword] = useState('')
const [isPasswordShown, setIsPasswordShown] = useState(false) const [isPasswordShown, setIsPasswordShown] = useState(false)
@@ -37,9 +40,9 @@ export const ModalLogin = () => {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
try { try {
const user = enteredUsername.split('@')[0] const [username, domain] = enteredUsername.split('@')
const response = await fetch( const response = await fetch(
'https://domain.com/.well-known/nostr.json?name=' + user, `https://${domain}/.well-known/nostr.json?name=${username}`,
) )
const getNpub: { const getNpub: {
names: { names: {
@@ -47,13 +50,13 @@ export const ModalLogin = () => {
} }
} = await response.json() } = await response.json()
const pubkey = getNpub.names[user] const pubkey = getNpub.names[username]
const npub = nip19.npubEncode(pubkey) const npub = nip19.npubEncode(pubkey)
const passphrase = enteredPassword const passphrase = enteredPassword
console.log('fetch', npub, passphrase) console.log('fetch', npub, passphrase)
const k: any = await swicCall('fetchKey', npub, passphrase) const k: any = await swicCall('fetchKey', npub, passphrase)
notify(`Fetched ${k.npub}`, 'success') notify(`Fetched ${k.npub}`, 'success')
navigate(`/key/${k.npub}`)
} catch (error: any) { } catch (error: any) {
notify(error.message, 'error') notify(error.message, 'error')
} }

View File

@@ -15,9 +15,15 @@ import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined' import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
import { ChangeEvent, useState } from 'react' import { ChangeEvent, useState } from 'react'
import { Checkbox } from '@/shared/Checkbox/Checkbox' import { Checkbox } from '@/shared/Checkbox/Checkbox'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { swicCall } from '@/modules/swic'
import { useParams } from 'react-router-dom'
export const ModalSettings = () => { export const ModalSettings = () => {
const { getModalOpened, handleClose } = useModalSearchParams() const { getModalOpened, handleClose } = useModalSearchParams()
const { npub = '' } = useParams<{ npub: string }>()
const notify = useEnqueueSnackbar()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SETTINGS) const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SETTINGS)
@@ -27,22 +33,16 @@ export const ModalSettings = () => {
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false) const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
const [isPasswordSynched, setIsPasswordSynched] = useState(false) const [isPasswordSynched, setIsPasswordSynched] = useState(false)
const [isChecked, setIsChecked] = useState(false)
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => { const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setIsPasswordInvalid(false)
setEnteredPassword(e.target.value) setEnteredPassword(e.target.value)
} }
const handlePasswordTypeChange = () => const handlePasswordTypeChange = () =>
setIsPasswordShown((prevState) => !prevState) setIsPasswordShown((prevState) => !prevState)
const handleSync = () => {
setIsPasswordInvalid(false)
if (enteredPassword.trim().length < 6) {
return setIsPasswordInvalid(true)
}
setIsPasswordSynched(true)
}
const onClose = () => { const onClose = () => {
handleCloseModal() handleCloseModal()
setEnteredPassword('') setEnteredPassword('')
@@ -50,10 +50,33 @@ export const ModalSettings = () => {
setIsPasswordSynched(false) setIsPasswordSynched(false)
} }
const handleChangeCheckbox = (e: unknown, checked: boolean) => {
setIsChecked(checked)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsPasswordInvalid(false)
setIsPasswordSynched(false)
if (enteredPassword.trim().length < 6) {
return setIsPasswordInvalid(true)
}
try {
await swicCall('saveKey', npub, enteredPassword)
notify('Key saved', 'success')
setIsPasswordInvalid(false)
setIsPasswordSynched(true)
} catch (error) {
setIsPasswordInvalid(false)
setIsPasswordSynched(false)
}
}
return ( return (
<Modal open={isModalOpened} onClose={onClose} title='Settings'> <Modal open={isModalOpened} onClose={onClose} title='Settings'>
<Stack gap={'1rem'}> <Stack gap={'1rem'}>
<StyledSettingContainer> <StyledSettingContainer onSubmit={handleSubmit}>
<Stack direction={'row'} justifyContent={'space-between'}> <Stack direction={'row'} justifyContent={'space-between'}>
<SectionTitle>Cloud sync</SectionTitle> <SectionTitle>Cloud sync</SectionTitle>
{isPasswordSynched && ( {isPasswordSynched && (
@@ -63,7 +86,10 @@ export const ModalSettings = () => {
)} )}
</Stack> </Stack>
<Box> <Box>
<Checkbox /> <Checkbox
onChange={handleChangeCheckbox}
checked={isChecked}
/>
<Typography variant='caption'> <Typography variant='caption'>
Use this login on multiple devices Use this login on multiple devices
</Typography> </Typography>
@@ -82,7 +108,7 @@ export const ModalSettings = () => {
)} )}
</IconButton> </IconButton>
} }
type={isPasswordShown ? 'password' : 'text'} type={isPasswordShown ? 'text' : 'password'}
onChange={handlePasswordChange} onChange={handlePasswordChange}
value={enteredPassword} value={enteredPassword}
helperText={isPasswordInvalid ? 'Invalid password' : ''} helperText={isPasswordInvalid ? 'Invalid password' : ''}
@@ -94,8 +120,9 @@ export const ModalSettings = () => {
}, },
}, },
}} }}
disabled={!isChecked}
/> />
<StyledButton type='button' fullWidth onClick={handleSync}> <StyledButton type='submit' fullWidth disabled={!isChecked}>
Sync Sync
</StyledButton> </StyledButton>
</StyledSettingContainer> </StyledSettingContainer>

View File

@@ -8,7 +8,7 @@ import {
} from '@mui/material' } from '@mui/material'
export const StyledSettingContainer = styled((props: StackProps) => ( export const StyledSettingContainer = styled((props: StackProps) => (
<Stack {...props} gap={'1rem'} /> <Stack gap={'1rem'} component={'form'} {...props} />
))(({ theme }) => ({ ))(({ theme }) => ({
padding: '0.75rem', padding: '0.75rem',
borderRadius: '1rem', borderRadius: '1rem',

View File

@@ -8,16 +8,18 @@ import { StyledAppLogo } from './styled'
import { Input } from '@/shared/Input/Input' import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button' import { Button } from '@/shared/Button/Button'
import { CheckmarkIcon } from '@/assets' import { CheckmarkIcon } from '@/assets'
import { swicCall } from '@/modules/swic'
import { useNavigate } from 'react-router-dom'
export const ModalSignUp = () => { export const ModalSignUp = () => {
const { getModalOpened, handleClose } = useModalSearchParams() const { getModalOpened, handleClose } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP)
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SIGN_UP) const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SIGN_UP)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const theme = useTheme() const theme = useTheme()
const navigate = useNavigate()
const [enteredValue, setEnteredValue] = useState('') const [enteredValue, setEnteredValue] = useState('')
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
@@ -36,6 +38,13 @@ export const ModalSignUp = () => {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
try {
const k: any = await swicCall('generateKey')
notify(`New key ${k.npub}`, 'success')
navigate(`/key/${k.npub}`)
} catch (error: any) {
notify(error.message, 'error')
}
} }
return ( return (
@@ -77,7 +86,9 @@ export const ModalSignUp = () => {
}, },
}} }}
/> />
<Button fullWidth>Sign up</Button> <Button fullWidth type='submit'>
Sign up
</Button>
</Stack> </Stack>
</Modal> </Modal>
) )

View File

@@ -7,7 +7,7 @@ import { useCallback, useEffect, useState } from 'react'
import { MetaEvent } from '@/types/meta-event' import { MetaEvent } from '@/types/meta-event'
import { fetchProfile } from '@/modules/nostr' import { fetchProfile } from '@/modules/nostr'
import { ProfileMenu } from './components/ProfileMenu' import { ProfileMenu } from './components/ProfileMenu'
import { getShortenNpub } from '@/utils/helpers' import { getShortenNpub } from '@/utils/helpers/helpers'
export const Header = () => { export const Header = () => {
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()

View File

@@ -1,5 +1,5 @@
import { DbKey } from '@/modules/db' import { DbKey } from '@/modules/db'
import { getShortenNpub } from '@/utils/helpers' import { getShortenNpub } from '@/utils/helpers/helpers'
import { import {
Avatar, Avatar,
ListItemIcon, ListItemIcon,

View File

@@ -2,6 +2,7 @@ import { Menu as MuiMenu } from '@mui/material'
import DarkModeIcon from '@mui/icons-material/DarkMode' import DarkModeIcon from '@mui/icons-material/DarkMode'
import LightModeIcon from '@mui/icons-material/LightMode' import LightModeIcon from '@mui/icons-material/LightMode'
import LoginIcon from '@mui/icons-material/Login' import LoginIcon from '@mui/icons-material/Login'
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
import { setThemeMode } from '@/store/reducers/ui.slice' import { setThemeMode } from '@/store/reducers/ui.slice'
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux' import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
@@ -10,13 +11,16 @@ import { MenuButton } from './styled'
import { useOpenMenu } from '@/hooks/useOpenMenu' import { useOpenMenu } from '@/hooks/useOpenMenu'
import { MenuItem } from './MenuItem' import { MenuItem } from './MenuItem'
import MenuRoundedIcon from '@mui/icons-material/MenuRounded' import MenuRoundedIcon from '@mui/icons-material/MenuRounded'
import { selectKeys } from '@/store'
export const Menu = () => { export const Menu = () => {
const themeMode = useAppSelector((state) => state.ui.themeMode) const themeMode = useAppSelector((state) => state.ui.themeMode)
const keys = useAppSelector(selectKeys)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { handleOpen } = useModalSearchParams() const { handleOpen } = useModalSearchParams()
const isDarkMode = themeMode === 'dark' const isDarkMode = themeMode === 'dark'
const isNoKeys = !keys || keys.length === 0
const { const {
anchorEl, anchorEl,
@@ -53,9 +57,11 @@ export const Menu = () => {
}} }}
> >
<MenuItem <MenuItem
Icon={<LoginIcon />} Icon={
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />
}
onClick={handleNavigateToAuth} onClick={handleNavigateToAuth}
title='Sign up' title={isNoKeys ? 'Sign up' : 'Add account'}
/> />
<MenuItem <MenuItem
Icon={themeIcon} Icon={themeIcon}

View File

@@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom'
import LoginIcon from '@mui/icons-material/Login' import LoginIcon from '@mui/icons-material/Login'
import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded' import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded'
import HomeRoundedIcon from '@mui/icons-material/HomeRounded' import HomeRoundedIcon from '@mui/icons-material/HomeRounded'
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux' import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
import { selectKeys } from '@/store' import { selectKeys } from '@/store'
import { setThemeMode } from '@/store/reducers/ui.slice' import { setThemeMode } from '@/store/reducers/ui.slice'
@@ -26,6 +27,7 @@ export const ProfileMenu = () => {
const { handleOpen } = useModalSearchParams() const { handleOpen } = useModalSearchParams()
const keys = useAppSelector(selectKeys) const keys = useAppSelector(selectKeys)
const isNoKeys = !keys || keys.length === 0
const themeMode = useAppSelector((state) => state.ui.themeMode) const themeMode = useAppSelector((state) => state.ui.themeMode)
const isDarkMode = themeMode === 'dark' const isDarkMode = themeMode === 'dark'
@@ -84,9 +86,11 @@ export const ProfileMenu = () => {
title='Home' title='Home'
/> />
<MenuItem <MenuItem
Icon={<LoginIcon />} Icon={
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />
}
onClick={handleNavigateToAuth} onClick={handleNavigateToAuth}
title='Sign up' title={isNoKeys ? 'Sign up' : 'Add account'}
/> />
<MenuItem <MenuItem
Icon={themeIcon} Icon={themeIcon}

View File

@@ -1,7 +0,0 @@
import React from 'react'
const AppPage = () => {
return <div>AppPage</div>
}
export default AppPage

View File

@@ -0,0 +1,132 @@
import { useLiveQuery } from 'dexie-react-hooks'
import { DbHistory, db } from '@/modules/db'
import { useParams } from 'react-router'
import { useAppSelector } from '@/store/hooks/redux'
import { selectAppByAppNpub, selectPermsByNpubAndAppNpub } from '@/store'
import { Navigate } from 'react-router-dom'
import { formatTimestampDate } from '@/utils/helpers/date'
import { Avatar, Box, IconButton, Stack, Typography } from '@mui/material'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
import { getShortenNpub } from '@/utils/helpers/helpers'
import { StyledButton } from './styled'
import { PermissionsMenu } from './components/PermissionsMenu'
import { useOpenMenu } from '@/hooks/useOpenMenu'
import { ACTIONS } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
import MoreIcon from '@mui/icons-material/MoreVert'
const getAppHistoryQuery = (appNpub: string) =>
db.history.where('appNpub').equals(appNpub).toArray()
const AppPage = () => {
const { appNpub = '', npub = '' } = useParams()
const perms = useAppSelector((state) =>
selectPermsByNpubAndAppNpub(state, npub, appNpub),
)
const currentApp = useAppSelector((state) =>
selectAppByAppNpub(state, appNpub),
)
const history = useLiveQuery(
() => {
if (!appNpub.trim().length) return []
return getAppHistoryQuery(appNpub)
},
[],
[] as DbHistory[],
)
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
const connectPerm = perms.find((perm) => perm.perm === 'connect')
if (!currentApp) {
return <Navigate to={`/key/${npub}`} />
}
const { icon = '', name = '' } = currentApp || {}
const appName = name || getShortenNpub(appNpub)
const { timestamp } = connectPerm || {}
const connectedOn =
connectPerm && timestamp
? `Connected at ${formatTimestampDate(timestamp)}`
: 'Not connected'
return (
<Stack maxHeight={'100%'} overflow={'auto'}>
<Stack
marginBottom={'1rem'}
direction={'row'}
gap={'1rem'}
width={'100%'}
>
<Avatar
src={icon}
sx={{
width: 70,
height: 70,
}}
variant='rounded'
/>
<Box flex={'1'} overflow={'hidden'}>
<Typography variant='h4' noWrap>
{appName}
</Typography>
<Typography variant='body2' noWrap>
{connectedOn}
</Typography>
</Box>
</Stack>
<Box marginBottom={'1rem'}>
<SectionTitle marginBottom={'0.5rem'}>Permissions</SectionTitle>
<StyledButton onClick={handleOpen}>
Basic/Advanced/Custom {perms.length}
</StyledButton>
<PermissionsMenu
open={open}
anchorEl={anchorEl}
perms={perms}
onClose={handleClose}
/>
</Box>
<SectionTitle marginBottom={'0.5rem'}>Activity</SectionTitle>
<Box
flex={1}
overflow={'auto'}
display={'flex'}
flexDirection={'column'}
gap={'0.5rem'}
>
{history.map((h) => {
return (
<Stack>
<Box
width={'100%'}
display={'flex'}
gap={'0.5rem'}
alignItems={'center'}
>
<Typography flex={1} fontWeight={700}>
{ACTIONS[h.method] || h.method}
</Typography>
<Typography
textTransform={'capitalize'}
variant='body2'
>
{h.allowed ? 'allow' : 'disallow'}
</Typography>
<IconButton>
<MoreIcon />
</IconButton>
</Box>
<Typography variant='caption'>
{formatTimestampDate(h.timestamp)}
</Typography>
</Stack>
)
})}
</Box>
</Stack>
)
}
export default AppPage

View File

@@ -0,0 +1,37 @@
import { FC } from 'react'
import { Box, Typography } from '@mui/material'
import { DbPerm } from '@/modules/db'
import { ACTIONS } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
import { StyledMenuItem } from './styled'
import { formatTimestampDate } from '@/utils/helpers/date'
type ItemPermissionMenuProps = {
permission: DbPerm
}
export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({
permission,
}) => {
const { perm, value, timestamp } = permission || {}
return (
<StyledMenuItem>
<Box
width={'100%'}
display={'flex'}
gap={'0.5rem'}
alignItems={'center'}
>
<Typography flex={1} fontWeight={700}>
{ACTIONS[perm] || perm}
</Typography>
<Typography textTransform={'capitalize'} variant='body2'>
{value === '1' ? 'allow' : 'disallow'}
</Typography>
</Box>
<Typography variant='body2'>
{formatTimestampDate(timestamp)}
</Typography>
</StyledMenuItem>
)
}

View File

@@ -0,0 +1,26 @@
import { DbPerm } from '@/modules/db'
import { Menu, MenuItem, MenuProps } from '@mui/material'
import { FC } from 'react'
import { ItemPermissionMenu } from './ItemPermissionMenu'
type PermissionsMenuProps = {
perms: DbPerm[]
} & MenuProps
export const PermissionsMenu: FC<PermissionsMenuProps> = ({
perms,
open,
anchorEl,
onClose,
}) => {
const isNoPerms = perms.length === 0
return (
<Menu open={open} anchorEl={anchorEl} onClose={onClose}>
{isNoPerms && <MenuItem>No permissions</MenuItem>}
{!isNoPerms &&
perms.map((perm) => (
<ItemPermissionMenu permission={perm} key={perm.id} />
))}
</Menu>
)
}

View File

@@ -0,0 +1,13 @@
import { MenuItem, MenuItemProps, styled } from '@mui/material'
export const StyledMenuItem = styled((props: MenuItemProps) => (
<MenuItem {...props} />
))(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '0.5rem',
'&:not(:first-of-type)': {
borderTop: '1px solid ' + theme.palette.secondary.main,
},
}))

View File

@@ -0,0 +1,6 @@
import { AppButtonProps, Button } from '@/shared/Button/Button'
import { styled } from '@mui/material'
export const StyledButton = styled((props: AppButtonProps) => (
<Button {...props} variant='outlined' fullWidth />
))(() => ({}))

View File

@@ -1,19 +1,43 @@
import { useAppSelector } from '../../store/hooks/redux' import { Fragment } from 'react'
import { selectKeys } from '../../store'
import { ItemKey } from './components/ItemKey' import { ItemKey } from './components/ItemKey'
import { Stack } from '@mui/material' import { Box, Stack, Typography } from '@mui/material'
import { SectionTitle } from '../../shared/SectionTitle/SectionTitle' import { AddAccountButton } from './styled'
import { useAppSelector } from '@/store/hooks/redux'
import { selectKeys } from '@/store'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
const HomePage = () => { const HomePage = () => {
const keys = useAppSelector(selectKeys) const keys = useAppSelector(selectKeys)
const isNoKeys = !keys || keys.length === 0
const { handleOpen } = useModalSearchParams()
const handleClickAddAccount = () => handleOpen(MODAL_PARAMS_KEYS.INITIAL)
return ( return (
<Stack> <Stack maxHeight={'100%'} overflow={'auto'}>
<SectionTitle marginBottom={'0.5rem'}>Keys:</SectionTitle> <SectionTitle marginBottom={'0.5rem'}>
<Stack gap={'0.5rem'}> {isNoKeys ? 'Welcome!' : 'Keys:'}
{keys.map((key) => ( </SectionTitle>
<ItemKey {...key} key={key.npub} /> <Stack gap={'0.5rem'} overflow={'auto'}>
))} {isNoKeys && (
<Typography textAlign={'center'} variant='h5'>
Hello, this is a key storage app for Nostr
</Typography>
)}
{!isNoKeys && (
<Fragment>
<Box flex={1} overflow={'auto'} borderRadius={'8px'}>
{keys.map((key) => (
<ItemKey {...key} key={key.npub} />
))}
</Box>
<AddAccountButton onClick={handleClickAddAccount}>
Add account
</AddAccountButton>
</Fragment>
)}
</Stack> </Stack>
</Stack> </Stack>
) )

View File

@@ -1,4 +1,4 @@
import { FC, useRef } from 'react' import { FC } from 'react'
import { DbKey } from '../../../modules/db' import { DbKey } from '../../../modules/db'
import { import {
Avatar, Avatar,
@@ -8,8 +8,7 @@ import {
TypographyProps, TypographyProps,
styled, styled,
} from '@mui/material' } from '@mui/material'
import { call, getShortenNpub, log } from '../../../utils/helpers' import { getShortenNpub } from '../../../utils/helpers/helpers'
import { swicCall } from '../../../modules/swic'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
type ItemKeyProps = DbKey type ItemKeyProps = DbKey
@@ -18,17 +17,6 @@ export const ItemKey: FC<ItemKeyProps> = (props) => {
const { npub, profile } = props const { npub, profile } = props
const navigate = useNavigate() const navigate = useNavigate()
const passPhraseInputRef = useRef<HTMLInputElement | null>(null)
// eslint-disable-next-line
async function saveKey(npub: string) {
call(async () => {
const passphrase = passPhraseInputRef.current?.value
await swicCall('saveKey', npub, passphrase)
log('Key saved')
})
}
const handleNavigate = () => { const handleNavigate = () => {
navigate('/key/' + npub) navigate('/key/' + npub)
} }

View File

@@ -0,0 +1,10 @@
import { AppButtonProps, Button } from '@/shared/Button/Button'
import { styled } from '@mui/material'
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
export const AddAccountButton = styled((props: AppButtonProps) => (
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
))(() => ({
alignSelf: 'center',
padding: '0.35rem 1rem',
}))

View File

@@ -1,10 +1,12 @@
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { SectionTitle } from '../../shared/SectionTitle/SectionTitle' import { SectionTitle } from '../../shared/SectionTitle/SectionTitle'
import { useAppSelector } from '../../store/hooks/redux' import { useAppSelector } from '../../store/hooks/redux'
import { askNotificationPermission, getShortenNpub } from '../../utils/helpers' import {
askNotificationPermission,
getShortenNpub,
} from '../../utils/helpers/helpers'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { fetchProfile } from '../../modules/nostr' import { fetchProfile } from '../../modules/nostr'
import { nip19 } from 'nostr-tools'
import { Badge, Box, CircularProgress, Stack } from '@mui/material' import { Badge, Box, CircularProgress, Stack } from '@mui/material'
import { StyledIconButton } from './styled' import { StyledIconButton } from './styled'
import { SettingsIcon, ShareIcon } from '@/assets' import { SettingsIcon, ShareIcon } from '@/assets'
@@ -52,7 +54,10 @@ const KeyPage = () => {
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const [profile, setProfile] = useState<MetaEvent | null>(null) const [profile, setProfile] = useState<MetaEvent | null>(null)
const userName = profile?.info?.name || profile?.info?.display_name || getShortenNpub(npub) const userName =
profile?.info?.name ||
profile?.info?.display_name ||
getShortenNpub(npub)
const userNameWithPrefix = userName + '@nsec.app' const userNameWithPrefix = userName + '@nsec.app'
const [showWarning, setShowWarning] = useState(false) const [showWarning, setShowWarning] = useState(false)

View File

@@ -5,7 +5,7 @@ import { Box, Stack, Typography } from '@mui/material'
import { FC } from 'react' import { FC } from 'react'
import { StyledEmptyAppsBox } from '../styled' import { StyledEmptyAppsBox } from '../styled'
import { Button } from '@/shared/Button/Button' import { Button } from '@/shared/Button/Button'
import { call } from '@/utils/helpers' import { call } from '@/utils/helpers/helpers'
import { swicCall } from '@/modules/swic' import { swicCall } from '@/modules/swic'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { ItemApp } from './ItemApp' import { ItemApp } from './ItemApp'

View File

@@ -3,7 +3,7 @@ import { Avatar, Stack, Typography } from '@mui/material'
import { FC } from 'react' import { FC } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
// import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined' // import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
import { getShortenNpub } from '@/utils/helpers' import { getShortenNpub } from '@/utils/helpers/helpers'
import { StyledItemAppContainer } from './styled' import { StyledItemAppContainer } from './styled'
type ItemAppProps = DbApp type ItemAppProps = DbApp

View File

@@ -7,7 +7,7 @@ import { CircularProgress, Stack } from '@mui/material'
const KeyPage = lazy(() => import('../pages/KeyPage/Key.Page')) const KeyPage = lazy(() => import('../pages/KeyPage/Key.Page'))
const ConfirmPage = lazy(() => import('../pages/Confirm.Page')) const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
const AppPage = lazy(() => import('../pages/App.Page')) const AppPage = lazy(() => import('../pages/AppPage/App.Page'))
const LoadingSpinner = () => ( const LoadingSpinner = () => (
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}> <Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>

View File

@@ -1,9 +1,17 @@
import { DialogProps, Slide } from '@mui/material' import { DialogProps, IconButton, Slide } from '@mui/material'
import { TransitionProps } from '@mui/material/transitions' import { TransitionProps } from '@mui/material/transitions'
import { FC, forwardRef } from 'react' import { FC, forwardRef } from 'react'
import { StyledDialog, StyledDialogContent, StyledDialogTitle } from './styled' import {
StyledCloseButtonWrapper,
StyledDialog,
StyledDialogContent,
StyledDialogTitle,
} from './styled'
import CloseRoundedIcon from '@mui/icons-material/CloseRounded'
type ModalProps = DialogProps type ModalProps = DialogProps & {
withCloseButton?: boolean
}
const Transition = forwardRef(function Transition( const Transition = forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
@@ -14,9 +22,29 @@ const Transition = forwardRef(function Transition(
return <Slide direction='up' ref={ref} {...props} /> return <Slide direction='up' ref={ref} {...props} />
}) })
export const Modal: FC<ModalProps> = ({ children, title, ...props }) => { export const Modal: FC<ModalProps> = ({
children,
title,
onClose,
withCloseButton = true,
...props
}) => {
return ( return (
<StyledDialog {...props} TransitionComponent={Transition}> <StyledDialog
{...props}
onClose={onClose}
TransitionComponent={Transition}
>
{withCloseButton && (
<StyledCloseButtonWrapper>
<IconButton
onClick={() => onClose && onClose({}, 'backdropClick')}
className='close_btn'
>
<CloseRoundedIcon />
</IconButton>
</StyledCloseButtonWrapper>
)}
{title && <StyledDialogTitle>{title}</StyledDialogTitle>} {title && <StyledDialogTitle>{title}</StyledDialogTitle>}
<StyledDialogContent>{children}</StyledDialogContent> <StyledDialogContent>{children}</StyledDialogContent>
</StyledDialog> </StyledDialog>

View File

@@ -1,4 +1,6 @@
import { import {
Box,
BoxProps,
Dialog, Dialog,
DialogContent, DialogContent,
DialogContentProps, DialogContentProps,
@@ -56,3 +58,17 @@ export const StyledDialogContent = styled((props: DialogContentProps) => (
padding: '0 1rem 1rem', padding: '0 1rem 1rem',
} }
}) })
export const StyledCloseButtonWrapper = styled((props: BoxProps) => (
<Box {...props} />
))(() => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: '0.5rem 1rem',
position: 'relative',
'& > .close_btn': {
position: 'absolute',
transform: 'translateY(50%)',
},
}))

View File

@@ -55,9 +55,25 @@ export const selectPermsByNpub = memoizeOne(
isDeepEqual, isDeepEqual,
) )
export const selectPermsByNpubAndAppNpub = memoizeOne(
(state: RootState, npub: string, appNpub: string) => {
return state.content.perms.filter(
(perm) => perm.npub === npub && perm.appNpub === appNpub,
)
},
isDeepEqual,
)
export const selectPendingsByNpub = memoizeOne( export const selectPendingsByNpub = memoizeOne(
(state: RootState, npub: string) => { (state: RootState, npub: string) => {
return state.content.pending.filter((pending) => pending.npub === npub) return state.content.pending.filter((pending) => pending.npub === npub)
}, },
isDeepEqual, isDeepEqual,
) )
export const selectAppByAppNpub = memoizeOne(
(state: RootState, appNpub: string) => {
return state.content.apps.find((app) => app.appNpub === appNpub)
},
isDeepEqual,
)

11
src/utils/helpers/date.ts Normal file
View File

@@ -0,0 +1,11 @@
import { format } from 'date-fns'
export const formatTimestampDate = (timestamp: number) => {
try {
const date = new Date(timestamp)
const formattedDate = format(date, "HH:mm',' dd-MM-yyyy")
return formattedDate
} catch (error) {
return ''
}
}

View File

@@ -1,5 +1,5 @@
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { NIP46_RELAYS } from './consts' import { NIP46_RELAYS } from '../consts'
export async function log(s: string) { export async function log(s: string) {
const log = document.getElementById('log') const log = document.getElementById('log')