diff --git a/src/webapp/app/(authenticated)/cards/add/page.tsx b/src/webapp/app/(authenticated)/cards/add/page.tsx
index 9fc22193..fb77ef41 100644
--- a/src/webapp/app/(authenticated)/cards/add/page.tsx
+++ b/src/webapp/app/(authenticated)/cards/add/page.tsx
@@ -4,13 +4,7 @@ import { useMemo } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { getAccessToken } from '@/services/auth';
import { ApiClient } from '@/api-client/ApiClient';
-import {
- Box,
- Stack,
- Text,
- Title,
- Card,
-} from '@mantine/core';
+import { Box, Stack, Text, Title, Card } from '@mantine/core';
import { UrlCardForm } from '@/components/UrlCardForm';
import { useAuth } from '@/hooks/useAuth';
@@ -18,16 +12,17 @@ export default function AddCardPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { user } = useAuth();
-
+
const preSelectedCollectionId = searchParams.get('collectionId');
// Create API client instance - memoized to avoid recreating on every render
const apiClient = useMemo(
- () => new ApiClient(
- process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
- () => getAccessToken(),
- ),
- []
+ () =>
+ new ApiClient(
+ process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
+ () => getAccessToken(),
+ ),
+ [],
);
const handleSuccess = () => {
@@ -43,9 +38,7 @@ export default function AddCardPage() {
Add Card
-
- Add a URL to your library with an optional note.
-
+ Add a URL to your library with an optional note.
diff --git a/src/webapp/app/(authenticated)/collections/[collectionId]/edit/page.tsx b/src/webapp/app/(authenticated)/collections/[collectionId]/edit/page.tsx
index 41f4890d..d395a0fe 100644
--- a/src/webapp/app/(authenticated)/collections/[collectionId]/edit/page.tsx
+++ b/src/webapp/app/(authenticated)/collections/[collectionId]/edit/page.tsx
@@ -126,7 +126,10 @@ export default function EditCollectionPage() {
{error && }
{success && (
-
+
)}
Edit Collection
-
+
@@ -146,7 +152,11 @@ export default function CollectionPage() {
) : (
No cards in this collection yet
-
diff --git a/src/webapp/app/(authenticated)/layout.tsx b/src/webapp/app/(authenticated)/layout.tsx
index 8a843651..e46c35f0 100644
--- a/src/webapp/app/(authenticated)/layout.tsx
+++ b/src/webapp/app/(authenticated)/layout.tsx
@@ -4,7 +4,14 @@ import { useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { useDisclosure, useMediaQuery } from '@mantine/hooks';
-import { ActionIcon, AppShell, Group, NavLink, Text, Affix } from '@mantine/core';
+import {
+ ActionIcon,
+ AppShell,
+ Group,
+ NavLink,
+ Text,
+ Affix,
+} from '@mantine/core';
import { FiSidebar } from 'react-icons/fi';
import { IoDocumentTextOutline } from 'react-icons/io5';
import { BsFolder2 } from 'react-icons/bs';
diff --git a/src/webapp/components/AddToCollectionModal.tsx b/src/webapp/components/AddToCollectionModal.tsx
index 410b16a2..9f1a1a4c 100644
--- a/src/webapp/components/AddToCollectionModal.tsx
+++ b/src/webapp/components/AddToCollectionModal.tsx
@@ -3,13 +3,7 @@
import { useState, useEffect, useMemo } from 'react';
import { getAccessToken } from '@/services/auth';
import { ApiClient } from '@/api-client/ApiClient';
-import {
- Button,
- Group,
- Modal,
- Stack,
- Text,
-} from '@mantine/core';
+import { Button, Group, Modal, Stack, Text } from '@mantine/core';
import { CollectionSelector } from './CollectionSelector';
interface Collection {
@@ -33,7 +27,9 @@ export function AddToCollectionModal({
onClose,
onSuccess,
}: AddToCollectionModalProps) {
- const [selectedCollectionIds, setSelectedCollectionIds] = useState([]);
+ const [selectedCollectionIds, setSelectedCollectionIds] = useState(
+ [],
+ );
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const [card, setCard] = useState(null);
@@ -141,7 +137,11 @@ export function AddToCollectionModal({
placeholder="Search collections to add..."
/>
- {error && {error}}
+ {error && (
+
+ {error}
+
+ )}
{
- return existingCollections.map(collection => collection.id);
+ return existingCollections.map((collection) => collection.id);
}, [existingCollections]);
// Collection search hook
@@ -60,19 +60,21 @@ export function CollectionSelector({
setSearchText,
handleSearchKeyPress,
loadCollections,
- } = useCollectionSearch({
- apiClient,
- initialLoad: true
+ } = useCollectionSearch({
+ apiClient,
+ initialLoad: true,
});
// Filter out existing collections from search results
const availableCollections = useMemo(() => {
- return allCollections.filter(collection => !existingCollectionIds.includes(collection.id));
+ return allCollections.filter(
+ (collection) => !existingCollectionIds.includes(collection.id),
+ );
}, [allCollections, existingCollectionIds]);
const handleCollectionToggle = (collectionId: string) => {
const newSelection = selectedCollectionIds.includes(collectionId)
- ? selectedCollectionIds.filter(id => id !== collectionId)
+ ? selectedCollectionIds.filter((id) => id !== collectionId)
: [...selectedCollectionIds, collectionId];
onSelectionChange(newSelection);
};
@@ -81,7 +83,10 @@ export function CollectionSelector({
setCreateModalOpen(true);
};
- const handleCreateCollectionSuccess = (collectionId: string, collectionName: string) => {
+ const handleCreateCollectionSuccess = (
+ collectionId: string,
+ collectionName: string,
+ ) => {
onSelectionChange([...selectedCollectionIds, collectionId]);
loadCollections(searchText.trim() || undefined);
setCreateModalOpen(false);
@@ -98,7 +103,8 @@ export function CollectionSelector({
{existingCollections.length > 0 && (
- Already in {existingCollections.length} collection{existingCollections.length !== 1 ? 's' : ''}:
+ Already in {existingCollections.length} collection
+ {existingCollections.length !== 1 ? 's' : ''}:
{existingCollections.map((collection) => (
@@ -109,11 +115,13 @@ export function CollectionSelector({
)}
-
+
- {existingCollections.length > 0 ? 'Add to additional collections (optional)' : 'Select collections (optional)'}
+ {existingCollections.length > 0
+ ? 'Add to additional collections (optional)'
+ : 'Select collections (optional)'}
-
+
0 ? (
- {availableCollections.length} collection{availableCollections.length !== 1 ? 's' : ''} found
+ {availableCollections.length} collection
+ {availableCollections.length !== 1 ? 's' : ''} found
{searchText.trim() && showCreateOption && (
) : (
- No collections found. You can create collections from your library.
+ No collections found. You can create collections from your
+ library.
)}
diff --git a/src/webapp/components/UrlCardForm.tsx b/src/webapp/components/UrlCardForm.tsx
index 94851955..7e6c1ca8 100644
--- a/src/webapp/components/UrlCardForm.tsx
+++ b/src/webapp/components/UrlCardForm.tsx
@@ -1,14 +1,7 @@
'use client';
import { useState, useMemo } from 'react';
-import {
- Stack,
- TextInput,
- Textarea,
- Button,
- Group,
- Text,
-} from '@mantine/core';
+import { Stack, TextInput, Textarea, Button, Group, Text } from '@mantine/core';
import { useForm } from '@mantine/form';
import { ApiClient } from '@/api-client/ApiClient';
import { UrlMetadataDisplay } from './UrlMetadataDisplay';
@@ -48,11 +41,16 @@ export function UrlCardForm({
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [selectedCollectionIds, setSelectedCollectionIds] = useState(
- preSelectedCollectionId ? [preSelectedCollectionId] : []
+ preSelectedCollectionId ? [preSelectedCollectionId] : [],
);
// URL metadata hook
- const { metadata, existingCard, loading: metadataLoading, error: metadataError } = useUrlMetadata({
+ const {
+ metadata,
+ existingCard,
+ loading: metadataLoading,
+ error: metadataError,
+ } = useUrlMetadata({
apiClient,
url: form.getValues().url,
autoFetch: !!form.getValues().url,
@@ -61,10 +59,11 @@ export function UrlCardForm({
// Get existing collections for this card (filtered by current user)
const existingCollections = useMemo(() => {
if (!existingCard || !userId) return [];
- return existingCard.collections.filter(collection => collection.authorId === userId);
+ return existingCard.collections.filter(
+ (collection) => collection.authorId === userId,
+ );
}, [existingCard, userId]);
-
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -89,7 +88,8 @@ export function UrlCardForm({
await apiClient.addUrlToLibrary({
url,
note: form.getValues().note.trim() || undefined,
- collectionIds: selectedCollectionIds.length > 0 ? selectedCollectionIds : undefined,
+ collectionIds:
+ selectedCollectionIds.length > 0 ? selectedCollectionIds : undefined,
});
onSuccess?.();
@@ -101,7 +101,6 @@ export function UrlCardForm({
}
};
-
return (
<>
-
>
);
}
diff --git a/src/webapp/components/UrlMetadataDisplay.tsx b/src/webapp/components/UrlMetadataDisplay.tsx
index 638a4dc7..b893268e 100644
--- a/src/webapp/components/UrlMetadataDisplay.tsx
+++ b/src/webapp/components/UrlMetadataDisplay.tsx
@@ -72,10 +72,10 @@ export function UrlMetadataDisplay({
)}
-
{metadata.title || 'Untitled'}
diff --git a/src/webapp/hooks/useCollectionSearch.ts b/src/webapp/hooks/useCollectionSearch.ts
index f4e207e1..75004458 100644
--- a/src/webapp/hooks/useCollectionSearch.ts
+++ b/src/webapp/hooks/useCollectionSearch.ts
@@ -8,40 +8,48 @@ interface UseCollectionSearchProps {
debounceMs?: number;
}
-export function useCollectionSearch({
- apiClient,
- initialLoad = true,
- debounceMs = 300
+export function useCollectionSearch({
+ apiClient,
+ initialLoad = true,
+ debounceMs = 300,
}: UseCollectionSearchProps) {
- const [collections, setCollections] = useState([]);
+ const [collections, setCollections] = useState<
+ GetMyCollectionsResponse['collections']
+ >([]);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
const [hasInitialized, setHasInitialized] = useState(false);
const debounceTimeoutRef = useRef(null);
// Memoized search parameters to avoid unnecessary API calls
- const searchParams = useMemo(() => ({
- limit: 20,
- sortBy: 'updatedAt' as const,
- sortOrder: 'desc' as const,
- }), []);
+ const searchParams = useMemo(
+ () => ({
+ limit: 20,
+ sortBy: 'updatedAt' as const,
+ sortOrder: 'desc' as const,
+ }),
+ [],
+ );
// Memoized load function that only changes when apiClient changes
- const loadCollections = useCallback(async (search?: string) => {
- setLoading(true);
- try {
- const response = await apiClient.getMyCollections({
- ...searchParams,
- searchText: search || undefined,
- });
- setCollections(response.collections);
- } catch (error) {
- console.error('Error loading collections:', error);
- // Don't clear collections on error, keep showing previous results
- } finally {
- setLoading(false);
- }
- }, [apiClient, searchParams]);
+ const loadCollections = useCallback(
+ async (search?: string) => {
+ setLoading(true);
+ try {
+ const response = await apiClient.getMyCollections({
+ ...searchParams,
+ searchText: search || undefined,
+ });
+ setCollections(response.collections);
+ } catch (error) {
+ console.error('Error loading collections:', error);
+ // Don't clear collections on error, keep showing previous results
+ } finally {
+ setLoading(false);
+ }
+ },
+ [apiClient, searchParams],
+ );
// Initial load effect - only runs once when component mounts
useEffect(() => {
@@ -80,7 +88,7 @@ export function useCollectionSearch({
if (debounceTimeoutRef.current) {
clearTimeout(debounceTimeoutRef.current);
}
-
+
const trimmedSearch = searchText.trim();
loadCollections(trimmedSearch || undefined);
}, [searchText, loadCollections]);
@@ -91,11 +99,14 @@ export function useCollectionSearch({
}, []);
// Handle search on Enter key press (immediate search, no debounce)
- const handleSearchKeyPress = useCallback((e: React.KeyboardEvent) => {
- if (e.key === 'Enter') {
- handleSearch();
- }
- }, [handleSearch]);
+ const handleSearchKeyPress = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ handleSearch();
+ }
+ },
+ [handleSearch],
+ );
return {
collections,
diff --git a/src/webapp/hooks/useExtensionAuth.tsx b/src/webapp/hooks/useExtensionAuth.tsx
index f9f4f945..bd1aa137 100644
--- a/src/webapp/hooks/useExtensionAuth.tsx
+++ b/src/webapp/hooks/useExtensionAuth.tsx
@@ -139,38 +139,40 @@ export const ExtensionAuthProvider = ({
}
}, [initAuth]);
- const loginWithAppPassword = useCallback(async (
- identifier: string,
- appPassword: string,
- ) => {
- try {
- setError(null);
- setIsLoading(true);
-
- // Use unauthenticated client for login
- const unauthenticatedClient = createApiClient(null);
- const response = await unauthenticatedClient.loginWithAppPassword({
- identifier,
- appPassword,
- });
- const { accessToken: newToken } = response;
-
- setAccessToken(newToken);
- await setStoredToken(newToken);
-
- // Create new authenticated client for profile fetch
- const authenticatedClient = createApiClient(newToken);
- const userData = await authenticatedClient.getMyProfile();
- setUser(userData);
- setIsAuthenticated(true);
- } catch (error: any) {
- console.error('App password login failed:', error);
- setError(error.message || 'Login failed. Please check your credentials.');
- throw error;
- } finally {
- setIsLoading(false);
- }
- }, [createApiClient, setStoredToken]);
+ const loginWithAppPassword = useCallback(
+ async (identifier: string, appPassword: string) => {
+ try {
+ setError(null);
+ setIsLoading(true);
+
+ // Use unauthenticated client for login
+ const unauthenticatedClient = createApiClient(null);
+ const response = await unauthenticatedClient.loginWithAppPassword({
+ identifier,
+ appPassword,
+ });
+ const { accessToken: newToken } = response;
+
+ setAccessToken(newToken);
+ await setStoredToken(newToken);
+
+ // Create new authenticated client for profile fetch
+ const authenticatedClient = createApiClient(newToken);
+ const userData = await authenticatedClient.getMyProfile();
+ setUser(userData);
+ setIsAuthenticated(true);
+ } catch (error: any) {
+ console.error('App password login failed:', error);
+ setError(
+ error.message || 'Login failed. Please check your credentials.',
+ );
+ throw error;
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [createApiClient, setStoredToken],
+ );
const logout = useCallback(async () => {
try {
diff --git a/src/webapp/hooks/useUrlMetadata.ts b/src/webapp/hooks/useUrlMetadata.ts
index 9382bf03..d467f0cf 100644
--- a/src/webapp/hooks/useUrlMetadata.ts
+++ b/src/webapp/hooks/useUrlMetadata.ts
@@ -9,50 +9,59 @@ interface UseUrlMetadataProps {
autoFetch?: boolean;
}
-export function useUrlMetadata({ apiClient, url, autoFetch = true }: UseUrlMetadataProps) {
+export function useUrlMetadata({
+ apiClient,
+ url,
+ autoFetch = true,
+}: UseUrlMetadataProps) {
const [metadata, setMetadata] = useState(null);
const [existingCard, setExistingCard] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
- const fetchMetadata = useCallback(async (targetUrl: string) => {
- if (!targetUrl.trim()) return;
+ const fetchMetadata = useCallback(
+ async (targetUrl: string) => {
+ if (!targetUrl.trim()) return;
- // Basic URL validation
- try {
- new URL(targetUrl);
- } catch {
- setError('Invalid URL format');
- return;
- }
+ // Basic URL validation
+ try {
+ new URL(targetUrl);
+ } catch {
+ setError('Invalid URL format');
+ return;
+ }
- setLoading(true);
- setError(null);
- setExistingCard(null);
+ setLoading(true);
+ setError(null);
+ setExistingCard(null);
- try {
- const response = await apiClient.getUrlMetadata(targetUrl);
- setMetadata(response.metadata);
+ try {
+ const response = await apiClient.getUrlMetadata(targetUrl);
+ setMetadata(response.metadata);
- // If there's an existing card, fetch its details including collections
- if (response.existingCardId) {
- try {
- const cardResponse = await apiClient.getUrlCardView(response.existingCardId);
- setExistingCard(cardResponse);
- } catch (cardErr: any) {
- console.error('Failed to fetch existing card details:', cardErr);
- // Don't set error here as the metadata fetch was successful
+ // If there's an existing card, fetch its details including collections
+ if (response.existingCardId) {
+ try {
+ const cardResponse = await apiClient.getUrlCardView(
+ response.existingCardId,
+ );
+ setExistingCard(cardResponse);
+ } catch (cardErr: any) {
+ console.error('Failed to fetch existing card details:', cardErr);
+ // Don't set error here as the metadata fetch was successful
+ }
}
+ } catch (err: any) {
+ console.error('Failed to fetch URL metadata:', err);
+ setError('Failed to load page information');
+ setMetadata(null);
+ setExistingCard(null);
+ } finally {
+ setLoading(false);
}
- } catch (err: any) {
- console.error('Failed to fetch URL metadata:', err);
- setError('Failed to load page information');
- setMetadata(null);
- setExistingCard(null);
- } finally {
- setLoading(false);
- }
- }, [apiClient]);
+ },
+ [apiClient],
+ );
// Auto-fetch when URL changes
useEffect(() => {