From c9c9563a1f04c5644f99d27175926eec6a93df34 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Fri, 3 Jul 2026 18:41:49 -0400 Subject: [PATCH] cleanup 4 --- frontend/AGENTS.md | 3 +- frontend/src/auth/AuthContext.tsx | 13 +- frontend/src/auth/authHelpers.ts | 10 +- frontend/src/components/Button.tsx | 5 +- frontend/src/components/IconInput.tsx | 19 ++ frontend/src/components/Table.tsx | 8 +- frontend/src/components/index.ts | 5 + frontend/src/hooks/useAuthForm.ts | 64 +++++++ frontend/src/lib/reader/EBookReader.ts | 22 +-- frontend/src/lib/reader/epubUtils.ts | 24 +-- frontend/src/pages/AdminImportPage.tsx | 170 ++++++++---------- frontend/src/pages/AdminImportResultsPage.tsx | 86 ++++----- frontend/src/pages/AdminLogsPage.tsx | 10 +- frontend/src/pages/AdminPage.tsx | 17 +- frontend/src/pages/AdminUsersPage.tsx | 28 +-- frontend/src/pages/DocumentPage.tsx | 4 +- frontend/src/pages/DocumentsPage.tsx | 9 +- frontend/src/pages/LoginPage.test.tsx | 2 +- frontend/src/pages/LoginPage.tsx | 55 +----- frontend/src/pages/ReaderPage.tsx | 14 +- frontend/src/pages/RegisterPage.test.tsx | 2 +- frontend/src/pages/RegisterPage.tsx | 45 ++--- frontend/src/pages/SearchPage.tsx | 54 ++---- frontend/src/pages/SettingsPage.tsx | 31 ++-- 24 files changed, 313 insertions(+), 387 deletions(-) create mode 100644 frontend/src/components/IconInput.tsx create mode 100644 frontend/src/hooks/useAuthForm.ts diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index aa07ca2..132aca9 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -41,7 +41,8 @@ Also follow the repository root guide at `../AGENTS.md`. - The generated client returns `{ data, status, headers }` for both success and error responses. - Do not assume non-2xx responses throw. -- Check `response.status` and response shape before treating a request as successful. +- Trust the generated discriminated-union response types and narrow on `status`; avoid hand-rolled re-validation of fields the schema already guarantees. +- Centralize mutation/reqest error handling via `useMutationWithToast` (toast options) or `getResponseError` (`utils/errors`) for imperative flows — don't re-implement inline `status` checks or `'message' in response.data` extraction. ## 4) Auth / Query State diff --git a/frontend/src/auth/AuthContext.tsx b/frontend/src/auth/AuthContext.tsx index ef3e76d..34d4bce 100644 --- a/frontend/src/auth/AuthContext.tsx +++ b/frontend/src/auth/AuthContext.tsx @@ -15,6 +15,7 @@ import { resolveAuthStateFromMe, authUserFromMutation, } from './authHelpers'; +import { getResponseError } from '../utils/errors'; interface AuthContextType extends AuthState { login: (_username: string, _password: string) => Promise; @@ -66,16 +67,16 @@ export function AuthProvider({ children }: { children: ReactNode }) { const user = authUserFromMutation(response); if (!user) { setAuthState(getUnauthenticatedAuthState()); - throw new Error('Login failed'); + throw new Error(getResponseError(response) ?? 'Login failed'); } setAuthState(getAuthenticatedAuthState(user)); await queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() }); navigate('/'); - } catch (_error) { + } catch (error) { setAuthState(getUnauthenticatedAuthState()); - throw new Error('Login failed'); + throw error instanceof Error ? error : new Error('Login failed'); } }, [loginMutation, navigate, queryClient] @@ -94,16 +95,16 @@ export function AuthProvider({ children }: { children: ReactNode }) { const user = authUserFromMutation(response); if (!user) { setAuthState(getUnauthenticatedAuthState()); - throw new Error('Registration failed'); + throw new Error(getResponseError(response) ?? 'Registration failed'); } setAuthState(getAuthenticatedAuthState(user)); await queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() }); navigate('/'); - } catch (_error) { + } catch (error) { setAuthState(getUnauthenticatedAuthState()); - throw new Error('Registration failed'); + throw error instanceof Error ? error : new Error('Registration failed'); } }, [navigate, queryClient, registerMutation] diff --git a/frontend/src/auth/authHelpers.ts b/frontend/src/auth/authHelpers.ts index a514ddc..9a98c44 100644 --- a/frontend/src/auth/authHelpers.ts +++ b/frontend/src/auth/authHelpers.ts @@ -1,8 +1,4 @@ -import type { - getMeResponse, - loginResponse, - registerResponse, -} from '../generated/anthoLumeAPIV1'; +import type { getMeResponse, loginResponse, registerResponse } from '../generated/anthoLumeAPIV1'; import type { LoginResponse } from '../generated/model'; export type AuthUser = LoginResponse; @@ -63,8 +59,6 @@ export function resolveAuthStateFromMe(params: { }; } -export function authUserFromMutation( - response: loginResponse | registerResponse -): AuthUser | null { +export function authUserFromMutation(response: loginResponse | registerResponse): AuthUser | null { return response.status === 200 || response.status === 201 ? response.data : null; } diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx index 21e4090..deb6e41 100644 --- a/frontend/src/components/Button.tsx +++ b/frontend/src/components/Button.tsx @@ -1,4 +1,5 @@ import { ButtonHTMLAttributes, forwardRef } from 'react'; +import { cn } from '../utils/cn'; interface BaseButtonProps { variant?: 'default' | 'secondary'; @@ -20,9 +21,9 @@ const getVariantClasses = (variant: 'default' | 'secondary' = 'default'): string }; export const Button = forwardRef( - ({ variant = 'default', children, className = '', ...props }, ref) => { + ({ variant = 'default', children, className, ...props }, ref) => { return ( - ); diff --git a/frontend/src/components/IconInput.tsx b/frontend/src/components/IconInput.tsx new file mode 100644 index 0000000..1d80272 --- /dev/null +++ b/frontend/src/components/IconInput.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from 'react'; +import { cn } from '../utils/cn'; + +interface IconInputProps { + icon: ReactNode; + children: ReactNode; + className?: string; +} + +export function IconInput({ icon, children, className }: IconInputProps) { + return ( +
+ + {icon} + + {children} +
+ ); +} diff --git a/frontend/src/components/Table.tsx b/frontend/src/components/Table.tsx index e37050d..f86ee1d 100644 --- a/frontend/src/components/Table.tsx +++ b/frontend/src/components/Table.tsx @@ -1,5 +1,6 @@ import { ReactNode } from 'react'; import { SkeletonTable } from './Skeleton'; +import { cn } from '../utils/cn'; export interface Column { id: string; @@ -46,7 +47,7 @@ export function Table({ {columns.map(column => ( {column.header} @@ -64,10 +65,7 @@ export function Table({ data.map((row, index) => ( {columns.map(column => ( - + {column.render(row, index)} ))} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 0b25b6b..b61ccd9 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -12,3 +12,8 @@ export { TextInput } from './TextInput'; // Field components export { Field, FieldLabel, FieldValue, FieldActions } from './Field'; + +// Button / Table +export { Button } from './Button'; +export { Table, type Column, type TableProps } from './Table'; +export { IconInput } from './IconInput'; diff --git a/frontend/src/hooks/useAuthForm.ts b/frontend/src/hooks/useAuthForm.ts new file mode 100644 index 0000000..af8eeb5 --- /dev/null +++ b/frontend/src/hooks/useAuthForm.ts @@ -0,0 +1,64 @@ +import { useCallback, useState, type SyntheticEvent } from 'react'; +import { useGetInfo } from '../generated/anthoLumeAPIV1'; +import { useAuth } from '../auth/AuthContext'; +import { useToasts } from '../components/ToastContext'; +import { getErrorMessage } from '../utils/errors'; + +export interface UseAuthFormResult { + username: string; + password: string; + isLoading: boolean; + isLoadingInfo: boolean; + registrationEnabled: boolean; + setUsername: (value: string) => void; + setPassword: (value: string) => void; + submit: (e: SyntheticEvent) => Promise; +} + +// Shared auth form state + submit for login/register. Server error messages are surfaced via the +// generated ErrorResponse contract rather than hardcoded fallbacks. +export function useAuthForm(mode: 'login' | 'register'): UseAuthFormResult { + const { login, register } = useAuth(); + const { showError } = useToasts(); + + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const { data: infoData, isLoading: isLoadingInfo } = useGetInfo({ + query: { staleTime: Infinity }, + }); + const registrationEnabled = infoData?.status === 200 ? infoData.data.registration_enabled : false; + + const submit = useCallback( + async (e: SyntheticEvent) => { + e.preventDefault(); + setIsLoading(true); + try { + if (mode === 'login') { + await login(username, password); + } else { + await register(username, password); + } + } catch (error) { + showError( + getErrorMessage(error, mode === 'login' ? 'Login failed' : 'Registration failed') + ); + } finally { + setIsLoading(false); + } + }, + [mode, login, register, username, password, showError] + ); + + return { + username, + password, + isLoading, + isLoadingInfo, + registrationEnabled, + setUsername, + setPassword, + submit, + }; +} diff --git a/frontend/src/lib/reader/EBookReader.ts b/frontend/src/lib/reader/EBookReader.ts index 203604b..fa4a151 100644 --- a/frontend/src/lib/reader/EBookReader.ts +++ b/frontend/src/lib/reader/EBookReader.ts @@ -268,10 +268,10 @@ export class EBookReader { private async setupReader() { this.bookState.words = await countWords(this.book); - const { cfi } = await getCFIFromXPath(this.book, this.rendition, this.bookState.progress); - await this.setPosition(cfi); - const { element } = await getCFIFromXPath(this.book, this.rendition, this.bookState.progress); - this.bookState.progressElement = element ?? null; + const cfiResult = await getCFIFromXPath(this.book, this.rendition, this.bookState.progress); + await this.setPosition(cfiResult?.cfi); + const elementResult = await getCFIFromXPath(this.book, this.rendition, this.bookState.progress); + this.bookState.progressElement = elementResult?.element ?? null; this.highlightPositionMarker(); const stats = await this.getBookStats(); this.onStats(stats); @@ -390,11 +390,11 @@ export class EBookReader { fontSize?: number; }) { const currentProgress = this.bookState.progress; - const { cfi } = await getCFIFromXPath(this.book, this.rendition, currentProgress); + const cfiResult = await getCFIFromXPath(this.book, this.rendition, currentProgress); this.setTheme(newTheme); - await this.setPosition(cfi); - const { element } = await getCFIFromXPath(this.book, this.rendition, currentProgress); - this.bookState.progressElement = element ?? null; + await this.setPosition(cfiResult?.cfi); + const elementResult = await getCFIFromXPath(this.book, this.rendition, currentProgress); + this.bookState.progressElement = elementResult?.element ?? null; this.highlightPositionMarker(); } @@ -449,10 +449,10 @@ export class EBookReader { async createProgress() { const currentCFI = await this.rendition.currentLocation(); - const { element, xpath } = await getXPathFromCFI(this.book, this.rendition, currentCFI.start.cfi); + const xpathResult = await getXPathFromCFI(this.book, this.rendition, currentCFI.start.cfi); const currentWord = await getBookWordPosition(this.book, this.rendition); - this.bookState.progress = xpath ?? ''; - this.bookState.progressElement = element ?? null; + this.bookState.progress = xpathResult?.xpath ?? ''; + this.bookState.progressElement = xpathResult?.element ?? null; const percentage = this.bookState.words > 0 diff --git a/frontend/src/lib/reader/epubUtils.ts b/frontend/src/lib/reader/epubUtils.ts index 88d7846..b19f50c 100644 --- a/frontend/src/lib/reader/epubUtils.ts +++ b/frontend/src/lib/reader/epubUtils.ts @@ -124,24 +124,28 @@ export async function getBookWordPosition(book: EpubBook, rendition: EpubRenditi return chapterWordPosition + preChapterWordPosition; } -export async function getXPathFromCFI(book: EpubBook, rendition: EpubRendition, cfi: string) { +export async function getXPathFromCFI( + book: EpubBook, + rendition: EpubRendition, + cfi: string +): Promise<{ xpath: string; element: Element | null } | null> { const cfiBaseMatch = cfi.match(/\(([^!]+)/); if (!cfiBaseMatch?.[1]) { - return {} as { xpath?: string; element?: Element | null }; + return null; } const startCFI = cfiBaseMatch[1]; const docFragmentIndex = (book.spine.spineItems.find(item => item.cfiBase === startCFI)?.index ?? -1) + 1; if (docFragmentIndex <= 0) { - return {} as { xpath?: string; element?: Element | null }; + return null; } const basePos = `/body/DocFragment[${docFragmentIndex}]/body`; const contents = rendition.getContents()[0]; const currentNodeStart = contents?.range(cfi).startContainer; if (!currentNodeStart) { - return {} as { xpath?: string; element?: Element | null }; + return null; } let currentNode: Node | null = currentNodeStart; @@ -181,23 +185,21 @@ export async function getCFIFromXPath( book: EpubBook, rendition: EpubRendition, xpath?: string -) { +): Promise<{ cfi: string; element: Element } | null> { if (!xpath) { - return {} as { cfi?: string; element?: Element | null }; + return null; } const fragMatch = xpath.match(/^\/body\/DocFragment\[(\d+)\]/); if (!fragMatch?.[1]) { - return {} as { cfi?: string; element?: Element | null }; + return null; } const spinePosition = Number.parseInt(fragMatch[1], 10) - 1; const sectionItem = book.spine.get(spinePosition); await sectionItem.load(book.load.bind(book)); - const renderedContent = rendition - .getContents() - .find(item => item.sectionIndex == spinePosition); + const renderedContent = rendition.getContents().find(item => item.sectionIndex == spinePosition); const docItem = renderedContent?.document || sectionItem.document; const namespaceURI = docItem.documentElement.namespaceURI; @@ -244,7 +246,7 @@ export async function getCFIFromXPath( const element = xpathElement || derivedSelectorElement; const isElementNode = Boolean(element && (element as Node).nodeType === Node.ELEMENT_NODE); if (!isElementNode) { - return {} as { cfi?: string; element?: Element | null }; + return null; } const resolvedElement = element as Element; diff --git a/frontend/src/pages/AdminImportPage.tsx b/frontend/src/pages/AdminImportPage.tsx index 324b1f6..1bc9963 100644 --- a/frontend/src/pages/AdminImportPage.tsx +++ b/frontend/src/pages/AdminImportPage.tsx @@ -1,6 +1,6 @@ -import { useState } from 'react'; +import { useState, type SyntheticEvent } from 'react'; import { useNavigate } from 'react-router-dom'; -import { LoadingState } from '../components'; +import { LoadingState, Table, type Column } from '../components'; import { useGetImportDirectory, usePostImport } from '../generated/anthoLumeAPIV1'; import type { DirectoryItem } from '../generated/model'; import { Button } from '../components/Button'; @@ -20,8 +20,7 @@ export default function AdminImportPage() { const postImport = usePostImport(); - const directoryResponse = - directoryData?.status === 200 ? directoryData.data : null; + const directoryResponse = directoryData?.status === 200 ? directoryData.data : null; const directories = directoryResponse?.items ?? []; const currentPathDisplay = directoryResponse?.current_path ?? currentPath ?? '/data'; @@ -37,7 +36,8 @@ export default function AdminImportPage() { } }; - const handleImport = () => { + const handleImport = (e: SyntheticEvent) => { + e.preventDefault(); if (!selectedDirectory) return; postImport.mutate( @@ -65,105 +65,83 @@ export default function AdminImportPage() { if (selectedDirectory) { return ( -
-
-
-

Selected Import Directory

-
-
-
- -

{selectedDirectory}

-
-
-
- setImportType('DIRECT')} - /> - -
-
- setImportType('COPY')} - /> - -
-
+
+

Selected Import Directory

+ +
+
+ +

{selectedDirectory}

+
+
+
+ setImportType('DIRECT')} + /> +
-
- - +
+ setImportType('COPY')} + /> +
- +
-
+
+ + +
+
); } + const directoryColumns: Column[] = [ + { + id: 'select', + header: '', + className: 'w-12', + render: item => ( + + ), + }, + { id: 'name', header: currentPathDisplay, render: item => item.name ?? '' }, + ]; + return ( -
-
- - - - - - - - - {currentPath !== '/' && ( - - - - - )} - {directories.length === 0 ? ( - - - - ) : ( - directories.map((item: DirectoryItem) => ( - - - - - )) - )} - -
- {currentPath} -
- -
- No Folders -
- - - -
-
+
+ {currentPathDisplay !== '/' && ( + + )} + item.name ?? ''} + /> ); } diff --git a/frontend/src/pages/AdminImportResultsPage.tsx b/frontend/src/pages/AdminImportResultsPage.tsx index 82048d5..cb41e78 100644 --- a/frontend/src/pages/AdminImportResultsPage.tsx +++ b/frontend/src/pages/AdminImportResultsPage.tsx @@ -1,67 +1,41 @@ import { useGetImportResults } from '../generated/anthoLumeAPIV1'; -import { LoadingState } from '../components'; +import { Table, type Column } from '../components'; import type { ImportResult } from '../generated/model'; import { Link } from 'react-router-dom'; export default function AdminImportResultsPage() { const { data: resultsData, isLoading } = useGetImportResults(); - const results = - resultsData?.status === 200 ? resultsData.data.results || [] : []; + const results = resultsData?.status === 200 ? (resultsData.data.results ?? []) : []; - if (isLoading) { - return ; - } + const columns: Column[] = [ + { + id: 'document', + header: 'Document', + render: result => ( +
+ Name: + {result.id ? ( + + {result.name} + + ) : ( + N/A + )} + File: + {result.path} +
+ ), + }, + { id: 'status', header: 'Status', render: result => result.status }, + { id: 'error', header: 'Error', render: result => result.error ?? '' }, + ]; return ( -
-
-
- - - - - - - - - {results.length === 0 ? ( - - - - ) : ( - results.map((result: ImportResult, index: number) => ( - - - - - - )) - )} - -
- Document - StatusError
- No Results -
- Name: - {result.id ? ( - - {result.name} - - ) : ( - N/A - )} - File: - {result.path} - -

{result.status}

-
-

{result.error || ''}

-
-
-
+ result.path ?? result.name ?? ''} + /> ); } diff --git a/frontend/src/pages/AdminLogsPage.tsx b/frontend/src/pages/AdminLogsPage.tsx index 238584d..d6b887c 100644 --- a/frontend/src/pages/AdminLogsPage.tsx +++ b/frontend/src/pages/AdminLogsPage.tsx @@ -1,7 +1,6 @@ import { SyntheticEvent } from 'react'; import { useGetLogs } from '../generated/anthoLumeAPIV1'; -import { Button } from '../components/Button'; -import { LoadingState, TextInput } from '../components'; +import { Button, LoadingState, TextInput, IconInput } from '../components'; import { useDebouncedState } from '../hooks/useDebouncedState'; import { Search2Icon } from '../icons'; @@ -22,10 +21,7 @@ export default function AdminLogsPage() {
-
- - - + }> -
+
+ ), }, { @@ -162,19 +167,19 @@ export default function AdminUsersPage() { {showAddForm && (
- setNewUsername(e.target.value)} placeholder="Username" - className="bg-surface p-2 text-content" + className="p-2" /> - setNewPassword(e.target.value)} placeholder="Password" - className="bg-surface p-2 text-content" + className="p-2" />
- +
)} diff --git a/frontend/src/pages/DocumentPage.tsx b/frontend/src/pages/DocumentPage.tsx index aead088..c8ab697 100644 --- a/frontend/src/pages/DocumentPage.tsx +++ b/frontend/src/pages/DocumentPage.tsx @@ -114,7 +114,9 @@ function EditableField({ export default function DocumentPage() { const { id } = useParams<{ id: string }>(); const queryClient = useQueryClient(); - const { data: docData, isLoading: docLoading } = useGetDocument(id || ''); + const { data: docData, isLoading: docLoading } = useGetDocument(id || '', { + query: { enabled: Boolean(id) }, + }); const editMutation = useEditDocument(); const { showError } = useToasts(); diff --git a/frontend/src/pages/DocumentsPage.tsx b/frontend/src/pages/DocumentsPage.tsx index 1b9de23..e2e60d2 100644 --- a/frontend/src/pages/DocumentsPage.tsx +++ b/frontend/src/pages/DocumentsPage.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import { useGetDocuments, useCreateDocument } from '../generated/anthoLumeAPIV1'; import type { Document } from '../generated/model'; import { ActivityIcon, DownloadIcon, Search2Icon, UploadIcon } from '../icons'; -import { LoadingState, Pagination, TextInput } from '../components'; +import { LoadingState, Pagination, TextInput, IconInput } from '../components'; import { useToasts } from '../components/ToastContext'; import { useMutationWithToast } from '../hooks/useMutationWithToast'; import { formatDuration } from '../utils/formatters'; @@ -160,10 +160,7 @@ export default function DocumentsPage() {
-
- - - + }> -
+
- ), + id: 'document', + header: 'Document', + render: item => `${item.author || 'N/A'} - ${item.title || 'N/A'}`, }, - { id: 'document', header: 'Document', render: item => `${item.author || 'N/A'} - ${item.title || 'N/A'}` }, { id: 'series', header: 'Series', render: item => item.series || 'N/A' }, { id: 'type', header: 'Type', render: item => item.file_type || 'N/A' }, { id: 'size', header: 'Size', render: item => item.file_size || 'N/A' }, @@ -42,26 +34,6 @@ interface SearchPageViewProps { onSubmit: (e: SyntheticEvent) => void; } -export function getSearchResults(data: unknown): SearchItem[] { - if (!data || typeof data !== 'object') { - return []; - } - - if (!('status' in data) || data.status !== 200) { - return []; - } - - if (!('data' in data) || !data.data || typeof data.data !== 'object') { - return []; - } - - if (!('results' in data.data) || !Array.isArray(data.data.results)) { - return []; - } - - return data.data.results as SearchItem[]; -} - export function SearchPageView({ query, source, @@ -77,22 +49,16 @@ export function SearchPageView({
-
- - - + }> onQueryChange(e.target.value)} placeholder="Query" /> -
+
-
- - - + }> -
+ @@ -126,7 +92,7 @@ export default function SearchPage() { }, } ); - const results = getSearchResults(data); + const results = data?.status === 200 ? (data.data.results ?? []) : []; const handleSubmit = (e: SyntheticEvent) => { e.preventDefault(); diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index f31a32f..956e929 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1,11 +1,9 @@ import { useState, useEffect, SyntheticEvent } from 'react'; -import { LoadingState, TextInput } from '../components'; +import { Button, LoadingState, Table, type Column, TextInput, IconInput } from '../components'; import { inputClassName } from '../components/TextInput'; -import { Table, type Column } from '../components/Table'; import { useGetSettings, useUpdateSettings } from '../generated/anthoLumeAPIV1'; import type { Device } from '../generated/model'; import { UserIcon, PasswordIcon, ClockIcon } from '../icons'; -import { Button } from '../components/Button'; import { useToasts } from '../components/ToastContext'; import { useMutationWithToast } from '../hooks/useMutationWithToast'; import { useTheme } from '../theme/ThemeProvider'; @@ -20,7 +18,11 @@ const deviceColumns: Column[] = [ className: 'pl-0', render: device => device.device_name || 'Unknown', }, - { id: 'last_synced', header: 'Last Sync', render: device => formatDeviceDate(device.last_synced) }, + { + id: 'last_synced', + header: 'Last Sync', + render: device => formatDeviceDate(device.last_synced), + }, { id: 'created_at', header: 'Created', render: device => formatDeviceDate(device.created_at) }, ]; @@ -99,30 +101,24 @@ export default function SettingsPage() {

Change Password

-
- - - + }> setPassword(e.target.value)} placeholder="Password" /> -
+
-
- - - + }> setNewPassword(e.target.value)} placeholder="New Password" /> -
+