This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / webapp / features / auth / components / loginForm / AppPasswordLoginForm.tsx
5.7 kB 199 lines
1'use client'; 2 3import { 4 Stack, 5 Text, 6 TextInput, 7 Button, 8 Alert, 9 UnstyledButton, 10 Combobox, 11 Loader, 12 Group, 13 Avatar, 14 ScrollArea, 15 PasswordInput, 16 Anchor, 17} from '@mantine/core'; 18import { MdLock, MdOutlineAlternateEmail } from 'react-icons/md'; 19import { BiRightArrowAlt } from 'react-icons/bi'; 20import { useCombobox } from '@mantine/core'; 21import { useState } from 'react'; 22import { useDebouncedValue } from '@mantine/hooks'; 23import { useQuery } from '@tanstack/react-query'; 24import { UseFormReturnType } from '@mantine/form'; 25import { searchBlueskyUsers } from '@/features/platforms/bluesky/lib/dal'; 26 27interface LoginFormValues { 28 handle: string; 29 appPassword: string; 30 useAppPassword: boolean; 31} 32 33type LoginFormType = UseFormReturnType<LoginFormValues>; 34 35interface Props { 36 form: LoginFormType; 37 error: string; 38 isLoading: boolean; 39 onSubmit: (e: React.FormEvent) => void; 40 onSwitchToOAuth: () => void; 41} 42 43export default function AppPasswordLoginForm(props: Props) { 44 const combobox = useCombobox({ 45 onDropdownClose: () => combobox.resetSelectedOption(), 46 }); 47 48 const [inputValue, setInputValue] = useState(props.form.values.handle); 49 const [debounced] = useDebouncedValue(inputValue, 200); 50 51 const { 52 data: actors = [], 53 isFetching, 54 error, 55 } = useQuery({ 56 queryKey: ['bluesky user search', debounced], 57 queryFn: () => searchBlueskyUsers(debounced), 58 enabled: debounced.trim().length > 0, 59 }); 60 61 const suggestions = actors; 62 const empty = 63 !error && 64 !isFetching && 65 debounced.trim().length > 0 && 66 suggestions.length === 0; 67 68 const options = suggestions.map((user) => ( 69 <Combobox.Option key={user.did} value={user.handle} p={5}> 70 <Group gap={'xs'} wrap="nowrap"> 71 <Avatar 72 src={user.avatar?.replace('avatar', 'avatar_thumbnail')} 73 alt={`${user.handle}'s avatar`} 74 /> 75 <Stack gap={0}> 76 <Text fw={500} c={'bright'} lineClamp={1}> 77 {user.displayName || user.handle} 78 </Text> 79 <Text fw={500} c={'gray'} lineClamp={1}> 80 @{user.handle} 81 </Text> 82 </Stack> 83 </Group> 84 </Combobox.Option> 85 )); 86 87 return ( 88 <Stack gap="xl"> 89 <form 90 onSubmit={(e) => { 91 combobox.closeDropdown(); 92 props.onSubmit(e); 93 }} 94 > 95 <Stack align="center"> 96 <Combobox 97 shadow="sm" 98 radius={'md'} 99 store={combobox} 100 withinPortal={false} 101 onOptionSubmit={(handleValue) => { 102 props.form.setFieldValue('handle', handleValue); 103 setInputValue(handleValue); 104 combobox.closeDropdown(); 105 }} 106 > 107 <Combobox.Target> 108 <TextInput 109 autoComplete="username" 110 label="Handle" 111 placeholder="you.bsky.social" 112 key={props.form.key('handle')} 113 value={inputValue} 114 onChange={(e) => { 115 const val = e.currentTarget.value; 116 setInputValue(val); 117 props.form.setFieldValue('handle', val); 118 combobox.openDropdown(); 119 }} 120 onFocus={() => combobox.openDropdown()} 121 onBlur={() => combobox.closeDropdown()} 122 leftSection={<MdOutlineAlternateEmail size={22} />} 123 rightSection={isFetching && <Loader size={18} />} 124 variant="filled" 125 size="lg" 126 w="100%" 127 miw={300} 128 required 129 /> 130 </Combobox.Target> 131 132 <Combobox.Dropdown 133 hidden={debounced.trim().length === 0 || isFetching} 134 > 135 <Combobox.Options> 136 <ScrollArea.Autosize type="scroll" mah={200}> 137 {error && ( 138 <Combobox.Empty> 139 Could not search for profiles 140 </Combobox.Empty> 141 )} 142 {empty && <Combobox.Empty>No profiles found</Combobox.Empty>} 143 {options.length > 0 && options} 144 </ScrollArea.Autosize> 145 </Combobox.Options> 146 </Combobox.Dropdown> 147 </Combobox> 148 149 <PasswordInput 150 autoComplete="password" 151 name="password" 152 label="App password" 153 placeholder="Your password" 154 key={props.form.key('appPassword')} 155 {...props.form.getInputProps('appPassword')} 156 leftSection={<MdLock size={22} />} 157 variant="filled" 158 size="lg" 159 w={'100%'} 160 required 161 /> 162 163 <Button 164 type="submit" 165 size="lg" 166 color="var(--mantine-color-dark-filled)" 167 fullWidth 168 rightSection={<BiRightArrowAlt size={22} />} 169 loading={props.isLoading} 170 > 171 Log in 172 </Button> 173 174 {props.error && <Alert title={props.error} color="red" />} 175 176 <Stack gap={0} align="center"> 177 <UnstyledButton 178 c={'gray'} 179 fw={600} 180 fz={'sm'} 181 onClick={props.onSwitchToOAuth} 182 > 183 Or{' '} 184 <Text c={'blue'} fw={500} fz={'sm'} span> 185 log in with OAuth 186 </Text> 187 </UnstyledButton> 188 <Text fw={500} fz={'sm'} c={'gray'}> 189 {"Don't have an account? "} 190 <Anchor href="/signup" fw={500} fz={'sm'} c={'blue'}> 191 Sign up 192 </Anchor> 193 </Text> 194 </Stack> 195 </Stack> 196 </form> 197 </Stack> 198 ); 199}