diff --git a/package-lock.json b/package-lock.json index b439f36..019491c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 7e372f1..d4de123 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 68b04dd..e1c2220 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -181,7 +181,7 @@ export const AppBar = () => { onClick={() => { setAnchorElUser(null) - navigate(appPrivateRoutes.settings) + navigate(appPrivateRoutes.profileSettings) }} sx={{ justifyContent: 'center' diff --git a/src/components/MarkTypeStrategy/DateTime/Input.tsx b/src/components/MarkTypeStrategy/DateTime/Input.tsx new file mode 100644 index 0000000..b2e864c --- /dev/null +++ b/src/components/MarkTypeStrategy/DateTime/Input.tsx @@ -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(null) + useEffect(() => { + if (ref.current) { + const date = new Date() + ref.current.value = date.toISOString().slice(0, 16) + handler(date.toUTCString()) + } + }, [handler]) + return ( + + ) +} diff --git a/src/components/MarkTypeStrategy/DateTime/index.tsx b/src/components/MarkTypeStrategy/DateTime/index.tsx new file mode 100644 index 0000000..1892d49 --- /dev/null +++ b/src/components/MarkTypeStrategy/DateTime/index.tsx @@ -0,0 +1,7 @@ +import { MarkStrategy } from '../MarkStrategy' +import { MarkInputDateTime } from './Input' + +export const DateTimeStrategy: MarkStrategy = { + input: MarkInputDateTime, + render: ({ value }) => <>{value} +} diff --git a/src/components/MarkTypeStrategy/FullName/Input.tsx b/src/components/MarkTypeStrategy/FullName/Input.tsx new file mode 100644 index 0000000..7b63ae6 --- /dev/null +++ b/src/components/MarkTypeStrategy/FullName/Input.tsx @@ -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) + } + }) +} diff --git a/src/components/MarkTypeStrategy/FullName/index.tsx b/src/components/MarkTypeStrategy/FullName/index.tsx new file mode 100644 index 0000000..1574c42 --- /dev/null +++ b/src/components/MarkTypeStrategy/FullName/index.tsx @@ -0,0 +1,7 @@ +import { MarkStrategy } from '../MarkStrategy' +import { MarkInputFullName } from './Input' + +export const FullNameStrategy: MarkStrategy = { + input: MarkInputFullName, + render: ({ value }) => <>{value} +} diff --git a/src/components/MarkTypeStrategy/JobTitle/Input.tsx b/src/components/MarkTypeStrategy/JobTitle/Input.tsx new file mode 100644 index 0000000..47d2969 --- /dev/null +++ b/src/components/MarkTypeStrategy/JobTitle/Input.tsx @@ -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) + } + }) +} diff --git a/src/components/MarkTypeStrategy/JobTitle/index.tsx b/src/components/MarkTypeStrategy/JobTitle/index.tsx new file mode 100644 index 0000000..11f5d60 --- /dev/null +++ b/src/components/MarkTypeStrategy/JobTitle/index.tsx @@ -0,0 +1,7 @@ +import { MarkStrategy } from '../MarkStrategy' +import { MarkInputJobTitle } from './Input' + +export const JobTitleStrategy: MarkStrategy = { + input: MarkInputJobTitle, + render: ({ value }) => <>{value} +} diff --git a/src/components/MarkTypeStrategy/MarkStrategy.tsx b/src/components/MarkTypeStrategy/MarkStrategy.tsx index 562302e..0ca0ebc 100644 --- a/src/components/MarkTypeStrategy/MarkStrategy.tsx +++ b/src/components/MarkTypeStrategy/MarkStrategy.tsx @@ -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 } diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss index 61983d7..92c044e 100644 --- a/src/components/PDFView/style.module.scss +++ b/src/components/PDFView/style.module.scss @@ -8,6 +8,4 @@ position: absolute; z-index: 40; display: flex; - justify-content: center; - align-items: center; } diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..a2a9532 --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -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( + key: string, + initialValue: T +): [T, React.Dispatch>] { + const getSnapshot = () => { + // Get the stored value + const storedValue = getLocalStorageItem(key, initialValue) + + // Parse the value + const parsedStoredValue = JSON.parse(storedValue) + + // Merge the default and the stored in case some of the required fields are missing + return JSON.stringify( + mergeWithInitialValue(parsedStoredValue, initialValue) + ) + } + + // https://react.dev/reference/react/useSyncExternalStore + // Returns the snapshot of the data and subscribes to the storage event + const data = React.useSyncExternalStore(useLocalStorageSubscribe, getSnapshot) + + // Takes the value or a function that returns the value and updates the local storage + const setState: React.Dispatch> = React.useCallback( + (v: React.SetStateAction) => { + 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] +} diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 8e1e8c0..e5b29f6 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -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 = () => { {isUsersOwnProfile && ( navigate(getProfileSettingsRoute(pubkey))} + onClick={() => navigate(appPrivateRoutes.profileSettings)} > diff --git a/src/pages/settings/Settings.tsx b/src/pages/settings/Settings.tsx index 5acdd9c..4bd9735 100644 --- a/src/pages/settings/Settings.tsx +++ b/src/pages/settings/Settings.tsx @@ -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 ( - <> - { + return ( + + {({ isActive }) => ( + + )} + + ) +} - {!disabled && ( - - )} - - ) - } +export const SettingsLayout = () => { + const { loginMethod } = useAppSelector((state) => state.auth) return ( <> - - Settings - - } - > - - - - - {listItem('Profile')} - - - - - - {listItem('Relays')} - - - - - - {listItem('Local Cache')} - - {loginMethod === LoginMethod.nostrLogin && ( - - - - - {listItem('Nostr Login')} - - )} - +

Settings

+
+
+ +
+
+ +
+