A social knowledge tool for researchers built on ATProto

formatting fixes

+205 -160
+9 -16
src/webapp/app/(authenticated)/cards/add/page.tsx
··· 4 4 import { useRouter, useSearchParams } from 'next/navigation'; 5 5 import { getAccessToken } from '@/services/auth'; 6 6 import { ApiClient } from '@/api-client/ApiClient'; 7 - import { 8 - Box, 9 - Stack, 10 - Text, 11 - Title, 12 - Card, 13 - } from '@mantine/core'; 7 + import { Box, Stack, Text, Title, Card } from '@mantine/core'; 14 8 import { UrlCardForm } from '@/components/UrlCardForm'; 15 9 import { useAuth } from '@/hooks/useAuth'; 16 10 ··· 18 12 const router = useRouter(); 19 13 const searchParams = useSearchParams(); 20 14 const { user } = useAuth(); 21 - 15 + 22 16 const preSelectedCollectionId = searchParams.get('collectionId'); 23 17 24 18 // Create API client instance - memoized to avoid recreating on every render 25 19 const apiClient = useMemo( 26 - () => new ApiClient( 27 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000', 28 - () => getAccessToken(), 29 - ), 30 - [] 20 + () => 21 + new ApiClient( 22 + process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000', 23 + () => getAccessToken(), 24 + ), 25 + [], 31 26 ); 32 27 33 28 const handleSuccess = () => { ··· 43 38 <Stack> 44 39 <Stack gap={0}> 45 40 <Title order={1}>Add Card</Title> 46 - <Text c="gray"> 47 - Add a URL to your library with an optional note. 48 - </Text> 41 + <Text c="gray">Add a URL to your library with an optional note.</Text> 49 42 </Stack> 50 43 51 44 <Card withBorder>
+4 -1
src/webapp/app/(authenticated)/collections/[collectionId]/edit/page.tsx
··· 126 126 {error && <Alert color="red" title={error} />} 127 127 128 128 {success && ( 129 - <Alert color="green" title="Collection updated successfully! Redirecting..." /> 129 + <Alert 130 + color="green" 131 + title="Collection updated successfully! Redirecting..." 132 + /> 130 133 )} 131 134 132 135 <TextInput
+12 -2
src/webapp/app/(authenticated)/collections/[collectionId]/page.tsx
··· 81 81 > 82 82 Edit Collection 83 83 </Button> 84 - <Button onClick={() => router.push(`/cards/add?collectionId=${collectionId}`)}>Add Card</Button> 84 + <Button 85 + onClick={() => 86 + router.push(`/cards/add?collectionId=${collectionId}`) 87 + } 88 + > 89 + Add Card 90 + </Button> 85 91 </Group> 86 92 </Group> 87 93 ··· 146 152 ) : ( 147 153 <Stack align="center"> 148 154 <Text c={'gray'}>No cards in this collection yet</Text> 149 - <Button onClick={() => router.push(`/cards/add?collectionId=${collectionId}`)}> 155 + <Button 156 + onClick={() => 157 + router.push(`/cards/add?collectionId=${collectionId}`) 158 + } 159 + > 150 160 Add Your First Card 151 161 </Button> 152 162 </Stack>
+8 -1
src/webapp/app/(authenticated)/layout.tsx
··· 4 4 import { usePathname, useRouter } from 'next/navigation'; 5 5 import { useAuth } from '@/hooks/useAuth'; 6 6 import { useDisclosure, useMediaQuery } from '@mantine/hooks'; 7 - import { ActionIcon, AppShell, Group, NavLink, Text, Affix } from '@mantine/core'; 7 + import { 8 + ActionIcon, 9 + AppShell, 10 + Group, 11 + NavLink, 12 + Text, 13 + Affix, 14 + } from '@mantine/core'; 8 15 import { FiSidebar } from 'react-icons/fi'; 9 16 import { IoDocumentTextOutline } from 'react-icons/io5'; 10 17 import { BsFolder2 } from 'react-icons/bs';
+9 -9
src/webapp/components/AddToCollectionModal.tsx
··· 3 3 import { useState, useEffect, useMemo } from 'react'; 4 4 import { getAccessToken } from '@/services/auth'; 5 5 import { ApiClient } from '@/api-client/ApiClient'; 6 - import { 7 - Button, 8 - Group, 9 - Modal, 10 - Stack, 11 - Text, 12 - } from '@mantine/core'; 6 + import { Button, Group, Modal, Stack, Text } from '@mantine/core'; 13 7 import { CollectionSelector } from './CollectionSelector'; 14 8 15 9 interface Collection { ··· 33 27 onClose, 34 28 onSuccess, 35 29 }: AddToCollectionModalProps) { 36 - const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>([]); 30 + const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>( 31 + [], 32 + ); 37 33 const [submitting, setSubmitting] = useState(false); 38 34 const [error, setError] = useState(''); 39 35 const [card, setCard] = useState<any>(null); ··· 141 137 placeholder="Search collections to add..." 142 138 /> 143 139 144 - {error && <Text c="red" size="sm">{error}</Text>} 140 + {error && ( 141 + <Text c="red" size="sm"> 142 + {error} 143 + </Text> 144 + )} 145 145 146 146 <Group grow> 147 147 <Button
+31 -19
src/webapp/components/CollectionSelector.tsx
··· 18 18 id: string; 19 19 name: string; 20 20 description?: string; 21 - cardCount: number; 21 + cardCount?: number; 22 22 authorId: string; 23 23 } 24 24 ··· 42 42 existingCollections = [], 43 43 disabled = false, 44 44 showCreateOption = true, 45 - placeholder = "Search collections...", 45 + placeholder = 'Search collections...', 46 46 preSelectedCollectionId, 47 47 }: CollectionSelectorProps) { 48 48 const [createModalOpen, setCreateModalOpen] = useState(false); 49 49 50 50 // Get existing collection IDs for filtering 51 51 const existingCollectionIds = useMemo(() => { 52 - return existingCollections.map(collection => collection.id); 52 + return existingCollections.map((collection) => collection.id); 53 53 }, [existingCollections]); 54 54 55 55 // Collection search hook ··· 60 60 setSearchText, 61 61 handleSearchKeyPress, 62 62 loadCollections, 63 - } = useCollectionSearch({ 64 - apiClient, 65 - initialLoad: true 63 + } = useCollectionSearch({ 64 + apiClient, 65 + initialLoad: true, 66 66 }); 67 67 68 68 // Filter out existing collections from search results 69 69 const availableCollections = useMemo(() => { 70 - return allCollections.filter(collection => !existingCollectionIds.includes(collection.id)); 70 + return allCollections.filter( 71 + (collection) => !existingCollectionIds.includes(collection.id), 72 + ); 71 73 }, [allCollections, existingCollectionIds]); 72 74 73 75 const handleCollectionToggle = (collectionId: string) => { 74 76 const newSelection = selectedCollectionIds.includes(collectionId) 75 - ? selectedCollectionIds.filter(id => id !== collectionId) 77 + ? selectedCollectionIds.filter((id) => id !== collectionId) 76 78 : [...selectedCollectionIds, collectionId]; 77 79 onSelectionChange(newSelection); 78 80 }; ··· 81 83 setCreateModalOpen(true); 82 84 }; 83 85 84 - const handleCreateCollectionSuccess = (collectionId: string, collectionName: string) => { 86 + const handleCreateCollectionSuccess = ( 87 + collectionId: string, 88 + collectionName: string, 89 + ) => { 85 90 onSelectionChange([...selectedCollectionIds, collectionId]); 86 91 loadCollections(searchText.trim() || undefined); 87 92 setCreateModalOpen(false); ··· 98 103 {existingCollections.length > 0 && ( 99 104 <Box> 100 105 <Text size="xs" c="dimmed" mb="xs"> 101 - Already in {existingCollections.length} collection{existingCollections.length !== 1 ? 's' : ''}: 106 + Already in {existingCollections.length} collection 107 + {existingCollections.length !== 1 ? 's' : ''}: 102 108 </Text> 103 109 <Group gap="xs"> 104 110 {existingCollections.map((collection) => ( ··· 109 115 </Group> 110 116 </Box> 111 117 )} 112 - 118 + 113 119 <Text size="sm" c="dimmed"> 114 - {existingCollections.length > 0 ? 'Add to additional collections (optional)' : 'Select collections (optional)'} 120 + {existingCollections.length > 0 121 + ? 'Add to additional collections (optional)' 122 + : 'Select collections (optional)'} 115 123 </Text> 116 - 124 + 117 125 <TextInput 118 126 placeholder={placeholder} 119 127 value={searchText} ··· 127 135 {availableCollections.length > 0 ? ( 128 136 <Stack gap={0}> 129 137 <Text size="xs" c="dimmed" mb="xs"> 130 - {availableCollections.length} collection{availableCollections.length !== 1 ? 's' : ''} found 138 + {availableCollections.length} collection 139 + {availableCollections.length !== 1 ? 's' : ''} found 131 140 </Text> 132 141 {searchText.trim() && showCreateOption && ( 133 142 <Box ··· 162 171 p="sm" 163 172 style={{ 164 173 cursor: 'pointer', 165 - backgroundColor: selectedCollectionIds.includes(collection.id) 166 - ? 'var(--mantine-color-blue-0)' 167 - : index % 2 === 0 168 - ? 'var(--mantine-color-gray-0)' 174 + backgroundColor: selectedCollectionIds.includes( 175 + collection.id, 176 + ) 177 + ? 'var(--mantine-color-blue-0)' 178 + : index % 2 === 0 179 + ? 'var(--mantine-color-gray-0)' 169 180 : 'transparent', 170 181 borderRadius: '4px', 171 182 border: selectedCollectionIds.includes(collection.id) ··· 237 248 </Stack> 238 249 ) : ( 239 250 <Text size="sm" c="dimmed" py="md" ta="center"> 240 - No collections found. You can create collections from your library. 251 + No collections found. You can create collections from your 252 + library. 241 253 </Text> 242 254 )} 243 255 </Box>
+13 -15
src/webapp/components/UrlCardForm.tsx
··· 1 1 'use client'; 2 2 3 3 import { useState, useMemo } from 'react'; 4 - import { 5 - Stack, 6 - TextInput, 7 - Textarea, 8 - Button, 9 - Group, 10 - Text, 11 - } from '@mantine/core'; 4 + import { Stack, TextInput, Textarea, Button, Group, Text } from '@mantine/core'; 12 5 import { useForm } from '@mantine/form'; 13 6 import { ApiClient } from '@/api-client/ApiClient'; 14 7 import { UrlMetadataDisplay } from './UrlMetadataDisplay'; ··· 48 41 const [loading, setLoading] = useState(false); 49 42 const [error, setError] = useState(''); 50 43 const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>( 51 - preSelectedCollectionId ? [preSelectedCollectionId] : [] 44 + preSelectedCollectionId ? [preSelectedCollectionId] : [], 52 45 ); 53 46 54 47 // URL metadata hook 55 - const { metadata, existingCard, loading: metadataLoading, error: metadataError } = useUrlMetadata({ 48 + const { 49 + metadata, 50 + existingCard, 51 + loading: metadataLoading, 52 + error: metadataError, 53 + } = useUrlMetadata({ 56 54 apiClient, 57 55 url: form.getValues().url, 58 56 autoFetch: !!form.getValues().url, ··· 61 59 // Get existing collections for this card (filtered by current user) 62 60 const existingCollections = useMemo(() => { 63 61 if (!existingCard || !userId) return []; 64 - return existingCard.collections.filter(collection => collection.authorId === userId); 62 + return existingCard.collections.filter( 63 + (collection) => collection.authorId === userId, 64 + ); 65 65 }, [existingCard, userId]); 66 - 67 66 68 67 const handleSubmit = async (e: React.FormEvent) => { 69 68 e.preventDefault(); ··· 89 88 await apiClient.addUrlToLibrary({ 90 89 url, 91 90 note: form.getValues().note.trim() || undefined, 92 - collectionIds: selectedCollectionIds.length > 0 ? selectedCollectionIds : undefined, 91 + collectionIds: 92 + selectedCollectionIds.length > 0 ? selectedCollectionIds : undefined, 93 93 }); 94 94 95 95 onSuccess?.(); ··· 100 100 setLoading(false); 101 101 } 102 102 }; 103 - 104 103 105 104 return ( 106 105 <> ··· 177 176 </Group> 178 177 </Stack> 179 178 </form> 180 - 181 179 </> 182 180 ); 183 181 }
+4 -4
src/webapp/components/UrlMetadataDisplay.tsx
··· 72 72 )} 73 73 <Stack gap="xs"> 74 74 <Stack gap={0}> 75 - <Title 76 - order={compact ? 4 : 3} 77 - lineClamp={2} 78 - fz={compact ? 'sm' : 'md'} 75 + <Title 76 + order={compact ? 4 : 3} 77 + lineClamp={2} 78 + fz={compact ? 'sm' : 'md'} 79 79 fw={500} 80 80 > 81 81 {metadata.title || 'Untitled'}
+42 -31
src/webapp/hooks/useCollectionSearch.ts
··· 8 8 debounceMs?: number; 9 9 } 10 10 11 - export function useCollectionSearch({ 12 - apiClient, 13 - initialLoad = true, 14 - debounceMs = 300 11 + export function useCollectionSearch({ 12 + apiClient, 13 + initialLoad = true, 14 + debounceMs = 300, 15 15 }: UseCollectionSearchProps) { 16 - const [collections, setCollections] = useState<GetMyCollectionsResponse['collections']>([]); 16 + const [collections, setCollections] = useState< 17 + GetMyCollectionsResponse['collections'] 18 + >([]); 17 19 const [loading, setLoading] = useState(false); 18 20 const [searchText, setSearchText] = useState(''); 19 21 const [hasInitialized, setHasInitialized] = useState(false); 20 22 const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null); 21 23 22 24 // Memoized search parameters to avoid unnecessary API calls 23 - const searchParams = useMemo(() => ({ 24 - limit: 20, 25 - sortBy: 'updatedAt' as const, 26 - sortOrder: 'desc' as const, 27 - }), []); 25 + const searchParams = useMemo( 26 + () => ({ 27 + limit: 20, 28 + sortBy: 'updatedAt' as const, 29 + sortOrder: 'desc' as const, 30 + }), 31 + [], 32 + ); 28 33 29 34 // Memoized load function that only changes when apiClient changes 30 - const loadCollections = useCallback(async (search?: string) => { 31 - setLoading(true); 32 - try { 33 - const response = await apiClient.getMyCollections({ 34 - ...searchParams, 35 - searchText: search || undefined, 36 - }); 37 - setCollections(response.collections); 38 - } catch (error) { 39 - console.error('Error loading collections:', error); 40 - // Don't clear collections on error, keep showing previous results 41 - } finally { 42 - setLoading(false); 43 - } 44 - }, [apiClient, searchParams]); 35 + const loadCollections = useCallback( 36 + async (search?: string) => { 37 + setLoading(true); 38 + try { 39 + const response = await apiClient.getMyCollections({ 40 + ...searchParams, 41 + searchText: search || undefined, 42 + }); 43 + setCollections(response.collections); 44 + } catch (error) { 45 + console.error('Error loading collections:', error); 46 + // Don't clear collections on error, keep showing previous results 47 + } finally { 48 + setLoading(false); 49 + } 50 + }, 51 + [apiClient, searchParams], 52 + ); 45 53 46 54 // Initial load effect - only runs once when component mounts 47 55 useEffect(() => { ··· 80 88 if (debounceTimeoutRef.current) { 81 89 clearTimeout(debounceTimeoutRef.current); 82 90 } 83 - 91 + 84 92 const trimmedSearch = searchText.trim(); 85 93 loadCollections(trimmedSearch || undefined); 86 94 }, [searchText, loadCollections]); ··· 91 99 }, []); 92 100 93 101 // Handle search on Enter key press (immediate search, no debounce) 94 - const handleSearchKeyPress = useCallback((e: React.KeyboardEvent) => { 95 - if (e.key === 'Enter') { 96 - handleSearch(); 97 - } 98 - }, [handleSearch]); 102 + const handleSearchKeyPress = useCallback( 103 + (e: React.KeyboardEvent) => { 104 + if (e.key === 'Enter') { 105 + handleSearch(); 106 + } 107 + }, 108 + [handleSearch], 109 + ); 99 110 100 111 return { 101 112 collections,
+31 -29
src/webapp/hooks/useExtensionAuth.tsx
··· 139 139 } 140 140 }, [initAuth]); 141 141 142 - const loginWithAppPassword = useCallback(async ( 143 - identifier: string, 144 - appPassword: string, 145 - ) => { 146 - try { 147 - setError(null); 148 - setIsLoading(true); 142 + const loginWithAppPassword = useCallback( 143 + async (identifier: string, appPassword: string) => { 144 + try { 145 + setError(null); 146 + setIsLoading(true); 149 147 150 - // Use unauthenticated client for login 151 - const unauthenticatedClient = createApiClient(null); 152 - const response = await unauthenticatedClient.loginWithAppPassword({ 153 - identifier, 154 - appPassword, 155 - }); 156 - const { accessToken: newToken } = response; 148 + // Use unauthenticated client for login 149 + const unauthenticatedClient = createApiClient(null); 150 + const response = await unauthenticatedClient.loginWithAppPassword({ 151 + identifier, 152 + appPassword, 153 + }); 154 + const { accessToken: newToken } = response; 157 155 158 - setAccessToken(newToken); 159 - await setStoredToken(newToken); 156 + setAccessToken(newToken); 157 + await setStoredToken(newToken); 160 158 161 - // Create new authenticated client for profile fetch 162 - const authenticatedClient = createApiClient(newToken); 163 - const userData = await authenticatedClient.getMyProfile(); 164 - setUser(userData); 165 - setIsAuthenticated(true); 166 - } catch (error: any) { 167 - console.error('App password login failed:', error); 168 - setError(error.message || 'Login failed. Please check your credentials.'); 169 - throw error; 170 - } finally { 171 - setIsLoading(false); 172 - } 173 - }, [createApiClient, setStoredToken]); 159 + // Create new authenticated client for profile fetch 160 + const authenticatedClient = createApiClient(newToken); 161 + const userData = await authenticatedClient.getMyProfile(); 162 + setUser(userData); 163 + setIsAuthenticated(true); 164 + } catch (error: any) { 165 + console.error('App password login failed:', error); 166 + setError( 167 + error.message || 'Login failed. Please check your credentials.', 168 + ); 169 + throw error; 170 + } finally { 171 + setIsLoading(false); 172 + } 173 + }, 174 + [createApiClient, setStoredToken], 175 + ); 174 176 175 177 const logout = useCallback(async () => { 176 178 try {
+42 -33
src/webapp/hooks/useUrlMetadata.ts
··· 9 9 autoFetch?: boolean; 10 10 } 11 11 12 - export function useUrlMetadata({ apiClient, url, autoFetch = true }: UseUrlMetadataProps) { 12 + export function useUrlMetadata({ 13 + apiClient, 14 + url, 15 + autoFetch = true, 16 + }: UseUrlMetadataProps) { 13 17 const [metadata, setMetadata] = useState<UrlMetadata | null>(null); 14 18 const [existingCard, setExistingCard] = useState<UrlCardView | null>(null); 15 19 const [loading, setLoading] = useState(false); 16 20 const [error, setError] = useState<string | null>(null); 17 21 18 - const fetchMetadata = useCallback(async (targetUrl: string) => { 19 - if (!targetUrl.trim()) return; 22 + const fetchMetadata = useCallback( 23 + async (targetUrl: string) => { 24 + if (!targetUrl.trim()) return; 20 25 21 - // Basic URL validation 22 - try { 23 - new URL(targetUrl); 24 - } catch { 25 - setError('Invalid URL format'); 26 - return; 27 - } 26 + // Basic URL validation 27 + try { 28 + new URL(targetUrl); 29 + } catch { 30 + setError('Invalid URL format'); 31 + return; 32 + } 28 33 29 - setLoading(true); 30 - setError(null); 31 - setExistingCard(null); 34 + setLoading(true); 35 + setError(null); 36 + setExistingCard(null); 32 37 33 - try { 34 - const response = await apiClient.getUrlMetadata(targetUrl); 35 - setMetadata(response.metadata); 38 + try { 39 + const response = await apiClient.getUrlMetadata(targetUrl); 40 + setMetadata(response.metadata); 36 41 37 - // If there's an existing card, fetch its details including collections 38 - if (response.existingCardId) { 39 - try { 40 - const cardResponse = await apiClient.getUrlCardView(response.existingCardId); 41 - setExistingCard(cardResponse); 42 - } catch (cardErr: any) { 43 - console.error('Failed to fetch existing card details:', cardErr); 44 - // Don't set error here as the metadata fetch was successful 42 + // If there's an existing card, fetch its details including collections 43 + if (response.existingCardId) { 44 + try { 45 + const cardResponse = await apiClient.getUrlCardView( 46 + response.existingCardId, 47 + ); 48 + setExistingCard(cardResponse); 49 + } catch (cardErr: any) { 50 + console.error('Failed to fetch existing card details:', cardErr); 51 + // Don't set error here as the metadata fetch was successful 52 + } 45 53 } 54 + } catch (err: any) { 55 + console.error('Failed to fetch URL metadata:', err); 56 + setError('Failed to load page information'); 57 + setMetadata(null); 58 + setExistingCard(null); 59 + } finally { 60 + setLoading(false); 46 61 } 47 - } catch (err: any) { 48 - console.error('Failed to fetch URL metadata:', err); 49 - setError('Failed to load page information'); 50 - setMetadata(null); 51 - setExistingCard(null); 52 - } finally { 53 - setLoading(false); 54 - } 55 - }, [apiClient]); 62 + }, 63 + [apiClient], 64 + ); 56 65 57 66 // Auto-fetch when URL changes 58 67 useEffect(() => {