This repository has no description
0

Configure Feed

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

feat: data access layer for all methods

+322 -295
+2 -6
src/webapp/app/(dashboard)/profile/[handle]/(withoutHeader)/collections/[rkey]/layout.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import BackButton from '@/components/navigation/backButton/BackButton'; 3 2 import Header from '@/components/navigation/header/Header'; 3 + import { getCollectionPageByAtUri } from '@/features/collections/lib/dal'; 4 4 import { truncateText } from '@/lib/utils/text'; 5 5 import type { Metadata } from 'next'; 6 6 import { Fragment } from 'react'; ··· 13 13 export async function generateMetadata({ params }: Props): Promise<Metadata> { 14 14 const { rkey, handle } = await params; 15 15 16 - const apiClient = new ApiClient( 17 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 18 - ); 19 - 20 - const collection = await apiClient.getCollectionPageByAtUri({ 16 + const collection = await getCollectionPageByAtUri({ 21 17 recordKey: rkey, 22 18 handle: handle, 23 19 });
+2 -6
src/webapp/app/(dashboard)/profile/[handle]/(withoutHeader)/collections/[rkey]/opengraph-image.tsx
··· 1 - import { ApiClient } from '@/api-client'; 1 + import { getCollectionPageByAtUri } from '@/features/collections/lib/dal'; 2 2 import OpenGraphCard from '@/features/openGraph/components/openGraphCard/OpenGraphCard'; 3 3 import { truncateText } from '@/lib/utils/text'; 4 4 ··· 15 15 export default async function Image(props: Props) { 16 16 const { rkey, handle } = await props.params; 17 17 18 - const apiClient = new ApiClient( 19 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 20 - ); 21 - 22 - const collection = await apiClient.getCollectionPageByAtUri({ 18 + const collection = await getCollectionPageByAtUri({ 23 19 recordKey: rkey, 24 20 handle: handle, 25 21 });
+1 -2
src/webapp/app/api/auth/logout/route.ts
··· 3 3 export async function POST(request: NextRequest) { 4 4 try { 5 5 // Proxy to backend to handle token revocation and cookie deletion 6 - const backendUrl = 7 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000'; 6 + const backendUrl = process.env.API_BASE_URL || 'http://127.0.0.1:3000'; 8 7 const backendResponse = await fetch(`${backendUrl}/api/users/logout`, { 9 8 method: 'POST', 10 9 headers: {
+2 -1
src/webapp/components/AddToCollectionModal.tsx
··· 4 4 import { ApiClient } from '@/api-client/ApiClient'; 5 5 import { Button, Group, Modal, Stack, Text } from '@mantine/core'; 6 6 import { CollectionSelector } from './CollectionSelector'; 7 + import { getUrlCardView } from '@/features/cards/lib/dal'; 7 8 8 9 interface Collection { 9 10 id: string; ··· 54 55 try { 55 56 setLoading(true); 56 57 setError(''); 57 - const response = await apiClient.getUrlCardView(cardId); 58 + const response = await getUrlCardView(cardId); 58 59 setCard(response); 59 60 } catch (error: any) { 60 61 console.error('Error fetching card:', error);
+2 -5
src/webapp/components/navigation/appLayout/AppLayout.tsx
··· 5 5 import ComposerDrawer from '@/features/composer/components/composerDrawer/ComposerDrawer'; 6 6 import { useNavbarContext } from '@/providers/navbar'; 7 7 import { usePathname } from 'next/navigation'; 8 - import { useAuth } from '@/hooks/useAuth'; 9 - import GuestNavbar from '../guestNavbar/GuestNavbar'; 10 8 11 9 interface Props { 12 10 children: React.ReactNode; ··· 14 12 15 13 export default function AppLayout(props: Props) { 16 14 const { mobileOpened, desktopOpened } = useNavbarContext(); 17 - const { isAuthenticated } = useAuth(); 18 15 const pathname = usePathname(); 19 16 20 17 const ROUTES_WITH_ASIDE = ['/url']; ··· 37 34 collapsed: { mobile: true }, 38 35 }} 39 36 > 40 - {isAuthenticated ? <Navbar /> : <GuestNavbar />} 37 + <Navbar /> 41 38 42 39 <AppShell.Main> 43 40 {props.children} 44 - {isAuthenticated && <ComposerDrawer />} 41 + <ComposerDrawer /> 45 42 </AppShell.Main> 46 43 </AppShell> 47 44 );
+41
src/webapp/components/navigation/guestAppLayout/GuestAppLayout.tsx
··· 1 + 'use client'; 2 + 3 + import { AppShell } from '@mantine/core'; 4 + import { useNavbarContext } from '@/providers/navbar'; 5 + import { usePathname } from 'next/navigation'; 6 + import GuestNavbar from '../guestNavbar/GuestNavbar'; 7 + 8 + interface Props { 9 + children: React.ReactNode; 10 + } 11 + 12 + export default function GuestAppLayout(props: Props) { 13 + const { mobileOpened, desktopOpened } = useNavbarContext(); 14 + const pathname = usePathname(); 15 + 16 + const ROUTES_WITH_ASIDE = ['/url']; 17 + const hasAside = ROUTES_WITH_ASIDE.some((prefix) => 18 + pathname.startsWith(prefix), 19 + ); 20 + const asideWidth = hasAside ? 300 : 0; 21 + 22 + return ( 23 + <AppShell 24 + header={{ height: 0 }} 25 + navbar={{ 26 + width: 300, 27 + breakpoint: 'xs', 28 + collapsed: { mobile: !mobileOpened, desktop: !desktopOpened }, 29 + }} 30 + aside={{ 31 + width: asideWidth, 32 + breakpoint: 'xl', 33 + collapsed: { mobile: true }, 34 + }} 35 + > 36 + <GuestNavbar /> 37 + 38 + <AppShell.Main>{props.children}</AppShell.Main> 39 + </AppShell> 40 + ); 41 + }
+5 -7
src/webapp/features/auth/components/loginForm/LoginForm.tsx
··· 1 1 'use client'; 2 2 3 3 import { ExtensionService } from '@/services/extensionService'; 4 - import { ApiClient } from '@/api-client/ApiClient'; 5 4 import { 6 5 Stack, 7 6 Text, ··· 17 16 import { useForm } from '@mantine/form'; 18 17 import { useRouter, useSearchParams } from 'next/navigation'; 19 18 import { useEffect, useState } from 'react'; 19 + import { createSembleClient } from '@/services/apiClient'; 20 20 21 21 export default function LoginForm() { 22 22 const router = useRouter(); ··· 28 28 const [error, setError] = useState(''); 29 29 30 30 const isExtensionLogin = searchParams.get('extension-login') === 'true'; 31 - const apiClient = new ApiClient( 32 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 33 - ); 31 + const client = createSembleClient(); 34 32 35 33 const handleExtensionTokenGeneration = async () => { 36 34 try { 37 35 setIsLoading(true); 38 - const tokens = await apiClient.generateExtensionTokens(); 36 + const tokens = await client.generateExtensionTokens(); 39 37 40 38 await ExtensionService.sendTokensToExtension(tokens); 41 39 ··· 82 80 ExtensionService.setExtensionTokensRequested(); 83 81 } 84 82 console.log('HANDLE', form.values.handle.trimEnd()); 85 - const { authUrl } = await apiClient.initiateOAuthSignIn({ 83 + const { authUrl } = await client.initiateOAuthSignIn({ 86 84 handle: form.values.handle.trimEnd(), 87 85 }); 88 86 ··· 105 103 setIsLoading(true); 106 104 setError(''); 107 105 108 - await apiClient.loginWithAppPassword({ 106 + await client.loginWithAppPassword({ 109 107 identifier: form.values.handle.trimEnd(), 110 108 appPassword: form.values.appPassword, 111 109 });
+89
src/webapp/features/cards/lib/dal.ts
··· 1 1 import { createSembleClient } from '@/services/apiClient'; 2 2 import { cache } from 'react'; 3 3 4 + interface PageParams { 5 + page?: number; 6 + limit?: number; 7 + } 8 + 4 9 export const getUrlMetadata = cache(async (url: string) => { 5 10 const client = createSembleClient(); 6 11 const response = await client.getUrlMetadata(url); ··· 9 14 }); 10 15 11 16 export const getCardFromMyLibrary = cache(async (url: string) => { 17 + // await verifySession(); 12 18 const client = createSembleClient(); 13 19 const response = await client.getUrlStatusForMyLibrary({ url: url }); 14 20 15 21 return response; 16 22 }); 23 + 24 + export const getMyUrlCards = cache(async (params?: PageParams) => { 25 + // await verifySession(); 26 + const client = createSembleClient(); 27 + const response = await client.getMyUrlCards({ 28 + page: params?.page, 29 + limit: params?.limit, 30 + }); 31 + 32 + return response; 33 + }); 34 + 35 + export const addUrlToLibrary = cache( 36 + async ( 37 + url: string, 38 + { note, collectionIds }: { note?: string; collectionIds?: string[] }, 39 + ) => { 40 + // await verifySession(); 41 + const client = createSembleClient(); 42 + const response = await client.addUrlToLibrary({ 43 + url: url, 44 + note: note, 45 + collectionIds: collectionIds, 46 + }); 47 + 48 + return response; 49 + }, 50 + ); 51 + 52 + export const getUrlCardView = cache(async (id: string) => { 53 + const client = createSembleClient(); 54 + const response = await client.getUrlCardView(id); 55 + 56 + return response; 57 + }); 58 + 59 + export const getUrlCards = cache( 60 + async (didOrHandle: string, params?: PageParams) => { 61 + const client = createSembleClient(); 62 + const response = await client.getUrlCards({ 63 + identifier: didOrHandle, 64 + page: params?.page, 65 + limit: params?.limit, 66 + }); 67 + 68 + return response; 69 + }, 70 + ); 71 + 72 + export const removeCardFromCollection = cache( 73 + async ({ 74 + cardId, 75 + collectionIds, 76 + }: { 77 + cardId: string; 78 + collectionIds: string[]; 79 + }) => { 80 + // await verifySession(); 81 + const client = createSembleClient(); 82 + const response = await client.removeCardFromCollection({ 83 + cardId, 84 + collectionIds, 85 + }); 86 + 87 + return response; 88 + }, 89 + ); 90 + 91 + export const removeCardFromLibrary = cache(async (cardId: string) => { 92 + // await verifySession(); 93 + const client = createSembleClient(); 94 + const response = await client.removeCardFromLibrary({ cardId }); 95 + 96 + return response; 97 + }); 98 + 99 + export const getLibrariesForCard = cache(async (cardId: string) => { 100 + // await verifySession(); 101 + const client = createSembleClient(); 102 + const response = await client.getLibrariesForCard(cardId); 103 + 104 + return response; 105 + });
+2 -7
src/webapp/features/cards/lib/mutations/useAddCard.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 + import { addUrlToLibrary } from '../dal'; 3 3 4 4 export default function useAddCard() { 5 - const apiClient = new ApiClient( 6 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 7 - ); 8 - 9 5 const queryClient = useQueryClient(); 10 6 11 7 const mutation = useMutation({ ··· 14 10 note?: string; 15 11 collectionIds?: string[]; 16 12 }) => { 17 - return apiClient.addUrlToLibrary({ 18 - url: newCard.url, 13 + return addUrlToLibrary(newCard.url, { 19 14 note: newCard.note, 20 15 collectionIds: newCard.collectionIds, 21 16 });
-39
src/webapp/features/cards/lib/mutations/useAddCardToLibrary.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 - import { useMutation, useQueryClient } from '@tanstack/react-query'; 3 - 4 - export default function useAddCardToLibrary() { 5 - const apiClient = new ApiClient( 6 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 7 - ); 8 - 9 - const queryClient = useQueryClient(); 10 - 11 - const mutation = useMutation({ 12 - mutationFn: ({ 13 - url, 14 - note, 15 - collectionIds, 16 - }: { 17 - url: string; 18 - note?: string; 19 - collectionIds: string[]; 20 - }) => { 21 - return apiClient.addUrlToLibrary({ url, note, collectionIds }); 22 - }, 23 - 24 - onSuccess: (data, variables) => { 25 - queryClient.invalidateQueries({ queryKey: ['card', data.urlCardId] }); 26 - queryClient.invalidateQueries({ queryKey: ['card', data.noteCardId] }); 27 - queryClient.invalidateQueries({ queryKey: ['card', variables.url] }); 28 - queryClient.invalidateQueries({ queryKey: ['my cards'] }); 29 - queryClient.invalidateQueries({ queryKey: ['home'] }); 30 - queryClient.invalidateQueries({ queryKey: ['collections'] }); 31 - 32 - variables.collectionIds.forEach((id) => { 33 - queryClient.invalidateQueries({ queryKey: ['collection', id] }); 34 - }); 35 - }, 36 - }); 37 - 38 - return mutation; 39 - }
+2 -6
src/webapp/features/cards/lib/mutations/useRemoveCardFromCollections.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 + import { removeCardFromCollection } from '../dal'; 3 3 4 4 export default function useRemoveCardFromCollections() { 5 - const apiClient = new ApiClient( 6 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 7 - ); 8 - 9 5 const queryClient = useQueryClient(); 10 6 11 7 const mutation = useMutation({ ··· 16 12 cardId: string; 17 13 collectionIds: string[]; 18 14 }) => { 19 - return apiClient.removeCardFromCollection({ cardId, collectionIds }); 15 + return removeCardFromCollection({ cardId, collectionIds }); 20 16 }, 21 17 22 18 onSuccess: (_data, variables) => {
+2 -6
src/webapp/features/cards/lib/mutations/useRemoveCardFromLibrary.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 + import { removeCardFromLibrary } from '../dal'; 3 3 4 4 export default function useRemoveCardFromLibrary() { 5 - const apiClient = new ApiClient( 6 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 7 - ); 8 - 9 5 const queryClient = useQueryClient(); 10 6 11 7 const mutation = useMutation({ 12 8 mutationFn: (cardId: string) => { 13 - return apiClient.removeCardFromLibrary({ cardId }); 9 + return removeCardFromLibrary(cardId); 14 10 }, 15 11 16 12 onSuccess: () => {
+2 -7
src/webapp/features/cards/lib/queries/useCards.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; 2 + import { getUrlCards } from '../dal'; 3 3 4 4 interface Props { 5 5 didOrHandle: string; ··· 7 7 } 8 8 9 9 export default function useCards(props: Props) { 10 - const apiClient = new ApiClient( 11 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 12 - ); 13 - 14 10 const limit = props?.limit ?? 16; 15 11 16 12 const cards = useSuspenseInfiniteQuery({ 17 13 queryKey: ['cards', props.didOrHandle, limit], 18 14 initialPageParam: 1, 19 15 queryFn: ({ pageParam = 1 }) => { 20 - return apiClient.getUrlCards({ 16 + return getUrlCards(props.didOrHandle, { 21 17 limit, 22 18 page: pageParam, 23 - identifier: props.didOrHandle, 24 19 }); 25 20 }, 26 21 getNextPageParam: (lastPage) => {
+2 -6
src/webapp/features/cards/lib/queries/useGetCard.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useSuspenseQuery } from '@tanstack/react-query'; 2 + import { getUrlCardView } from '../dal'; 3 3 4 4 interface Props { 5 5 id: string; 6 6 } 7 7 8 8 export default function useGetCard(props: Props) { 9 - const apiClient = new ApiClient( 10 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 11 - ); 12 - 13 9 const card = useSuspenseQuery({ 14 10 queryKey: ['card', props.id], 15 - queryFn: () => apiClient.getUrlCardView(props.id), 11 + queryFn: () => getUrlCardView(props.id), 16 12 }); 17 13 18 14 return card;
+2 -6
src/webapp/features/cards/lib/queries/useGetLibrariesForcard.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useSuspenseQuery } from '@tanstack/react-query'; 2 + import { getLibrariesForCard } from '../dal'; 3 3 4 4 interface Props { 5 5 id: string; 6 6 } 7 7 8 8 export default function useGetLibrariesForCard(props: Props) { 9 - const apiClient = new ApiClient( 10 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 11 - ); 12 - 13 9 const libraries = useSuspenseQuery({ 14 10 queryKey: ['libraries for card', props.id], 15 - queryFn: () => apiClient.getLibrariesForCard(props.id), 11 + queryFn: () => getLibrariesForCard(props.id), 16 12 }); 17 13 18 14 return libraries;
+2 -6
src/webapp/features/cards/lib/queries/useMyCards.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; 2 + import { getMyUrlCards } from '../dal'; 3 3 4 4 interface Props { 5 5 limit?: number; 6 6 } 7 7 8 8 export default function useMyCards(props?: Props) { 9 - const apiClient = new ApiClient( 10 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 11 - ); 12 - 13 9 const limit = props?.limit ?? 16; 14 10 15 11 const myCards = useSuspenseInfiniteQuery({ 16 12 queryKey: ['my cards', limit], 17 13 initialPageParam: 1, 18 14 queryFn: ({ pageParam = 1 }) => { 19 - return apiClient.getMyUrlCards({ limit, page: pageParam }); 15 + return getMyUrlCards({ page: pageParam, limit }); 20 16 }, 21 17 getNextPageParam: (lastPage) => { 22 18 if (lastPage.pagination.hasMore) {
+4 -1
src/webapp/features/collections/components/collectionActions/CollectionActions.tsx
··· 20 20 const [showDeleteModal, setShowDeleteModal] = useState(false); 21 21 22 22 const isAuthor = user?.handle === props.authorHandle; 23 - const shareLink = `${window.location.origin}/profile/${props.authorHandle}/collections/${props.rkey}`; 23 + const shareLink = 24 + typeof window !== 'undefined' 25 + ? `${window.location.origin}/profile/${props.authorHandle}/collections/${props.rkey}` 26 + : ''; 24 27 25 28 if (!isAuthenticated) { 26 29 return null;
+86 -5
src/webapp/features/collections/lib/dal.ts
··· 6 6 limit?: number; 7 7 } 8 8 9 + interface SearchParams { 10 + sortBy?: string; 11 + sortOrder?: 'asc' | 'desc'; 12 + searchText?: string; 13 + } 14 + 9 15 export const getCollectionsForUrl = cache( 10 16 async (url: string, params?: PageParams) => { 11 17 const client = createSembleClient(); ··· 19 25 }, 20 26 ); 21 27 22 - export const getMyCollections = cache(async (params?: PageParams) => { 28 + export const getCollections = cache( 29 + async (didOrHandle: string, params?: PageParams) => { 30 + const client = createSembleClient(); 31 + const response = await client.getCollections({ 32 + identifier: didOrHandle, 33 + limit: params?.limit, 34 + page: params?.page, 35 + }); 36 + 37 + return response; 38 + }, 39 + ); 40 + 41 + export const getMyCollections = cache( 42 + async (params?: PageParams & SearchParams) => { 43 + // await verifySession(); 44 + 45 + const client = createSembleClient(); 46 + const response = await client.getMyCollections({ 47 + page: params?.page, 48 + limit: params?.limit, 49 + sortBy: params?.sortBy, 50 + sortOrder: params?.sortOrder, 51 + searchText: params?.searchText, 52 + }); 53 + 54 + return response; 55 + }, 56 + ); 57 + 58 + export const createCollection = cache( 59 + async (newCollection: { name: string; description: string }) => { 60 + // await verifySession(); 61 + const client = createSembleClient(); 62 + const response = await client.createCollection(newCollection); 63 + 64 + return response; 65 + }, 66 + ); 67 + 68 + export const deleteCollection = cache(async (id: string) => { 69 + // await verifySession(); 23 70 const client = createSembleClient(); 24 - const response = await client.getMyCollections({ 25 - page: params?.page, 26 - limit: params?.limit, 27 - }); 71 + const response = await client.deleteCollection({ collectionId: id }); 28 72 29 73 return response; 30 74 }); 75 + 76 + export const updateCollection = cache( 77 + async (collection: { 78 + collectionId: string; 79 + rkey: string; 80 + name: string; 81 + description?: string; 82 + }) => { 83 + // await verifySession(); 84 + const client = createSembleClient(); 85 + const response = await client.updateCollection(collection); 86 + 87 + return response; 88 + }, 89 + ); 90 + 91 + export const getCollectionPageByAtUri = cache( 92 + async ({ 93 + recordKey, 94 + handle, 95 + params, 96 + }: { 97 + recordKey: string; 98 + handle: string; 99 + params?: PageParams; 100 + }) => { 101 + const client = createSembleClient(); 102 + const response = await client.getCollectionPageByAtUri({ 103 + recordKey, 104 + handle, 105 + page: params?.page, 106 + limit: params?.limit, 107 + }); 108 + 109 + return response; 110 + }, 111 + );
-35
src/webapp/features/collections/lib/mutations/useAddCardToCollection.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 - import { useMutation, useQueryClient } from '@tanstack/react-query'; 3 - 4 - export default function useAddCardToCollection() { 5 - const apiClient = new ApiClient( 6 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 7 - ); 8 - 9 - const queryClient = useQueryClient(); 10 - 11 - const mutation = useMutation({ 12 - mutationFn: ({ 13 - cardId, 14 - collectionIds, 15 - }: { 16 - cardId: string; 17 - collectionIds: string[]; 18 - }) => { 19 - return apiClient.addCardToCollection({ cardId, collectionIds }); 20 - }, 21 - 22 - onSuccess: (_data, variables) => { 23 - queryClient.invalidateQueries({ queryKey: ['card', variables.cardId] }); 24 - queryClient.invalidateQueries({ queryKey: ['my cards'] }); 25 - queryClient.invalidateQueries({ queryKey: ['home'] }); 26 - queryClient.invalidateQueries({ queryKey: ['collections'] }); 27 - 28 - variables.collectionIds.forEach((id) => { 29 - queryClient.invalidateQueries({ queryKey: ['collection', id] }); 30 - }); 31 - }, 32 - }); 33 - 34 - return mutation; 35 - }
+2 -6
src/webapp/features/collections/lib/mutations/useCreateCollection.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 + import { createCollection } from '../dal'; 3 3 4 4 export default function useCreateCollection() { 5 - const apiClient = new ApiClient( 6 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 7 - ); 8 - 9 5 const queryClient = useQueryClient(); 10 6 11 7 const mutation = useMutation({ 12 8 mutationFn: (newCollection: { name: string; description: string }) => { 13 - return apiClient.createCollection(newCollection); 9 + return createCollection(newCollection); 14 10 }, 15 11 16 12 // Do things that are absolutely necessary and logic related (like query invalidation) in the useMutation callbacks
+2 -6
src/webapp/features/collections/lib/mutations/useDeleteCollection.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 + import { deleteCollection } from '../dal'; 3 3 4 4 export default function useDeleteCollection() { 5 - const apiClient = new ApiClient( 6 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 7 - ); 8 - 9 5 const queryClient = useQueryClient(); 10 6 11 7 const mutation = useMutation({ 12 8 mutationFn: (collectionId: string) => { 13 - return apiClient.deleteCollection({ collectionId }); 9 + return deleteCollection(collectionId); 14 10 }, 15 11 16 12 onSuccess: (data) => {
+2 -6
src/webapp/features/collections/lib/mutations/useUpdateCollection.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 + import { updateCollection } from '../dal'; 3 3 4 4 export default function useUpdateCollection() { 5 - const apiClient = new ApiClient( 6 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 7 - ); 8 - 9 5 const queryClient = useQueryClient(); 10 6 11 7 const mutation = useMutation({ ··· 15 11 name: string; 16 12 description?: string; 17 13 }) => { 18 - return apiClient.updateCollection(collection); 14 + return updateCollection(collection); 19 15 }, 20 16 21 17 onSuccess: (data, variables) => {
+3 -6
src/webapp/features/collections/lib/queries/useCollection.tsx
··· 1 - import { createSembleClient } from '@/services/apiClient'; 2 1 import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; 2 + import { getCollectionPageByAtUri } from '../dal'; 3 3 4 4 interface Props { 5 5 rkey: string; ··· 8 8 } 9 9 10 10 export default function useCollection(props: Props) { 11 - const apiClient = createSembleClient(); 12 - 13 11 const limit = props.limit ?? 20; 14 12 15 13 return useSuspenseInfiniteQuery({ 16 14 queryKey: ['collection', props.rkey, props.handle, limit], 17 15 initialPageParam: 1, 18 16 queryFn: ({ pageParam }) => 19 - apiClient.getCollectionPageByAtUri({ 17 + getCollectionPageByAtUri({ 20 18 recordKey: props.rkey, 21 19 handle: props.handle, 22 - limit, 23 - page: pageParam, 20 + params: { limit, page: pageParam }, 24 21 }), 25 22 getNextPageParam: (lastPage) => { 26 23 return lastPage.pagination.hasMore
+2 -6
src/webapp/features/collections/lib/queries/useCollectionSearch.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useQuery } from '@tanstack/react-query'; 2 + import { getMyCollections } from '../dal'; 3 3 4 4 interface Props { 5 5 query: string; ··· 9 9 } 10 10 11 11 export default function useCollectionSearch(props: Props) { 12 - const apiClient = new ApiClient( 13 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 14 - ); 15 - 16 12 // TODO: replace with infinite suspense query 17 13 const collections = useQuery({ 18 14 queryKey: ['collection search', props.query, props.params?.limit], 19 15 queryFn: () => 20 - apiClient.getMyCollections({ 16 + getMyCollections({ 21 17 limit: props.params?.limit ?? 10, 22 18 sortBy: 'updatedAt', 23 19 sortOrder: 'desc',
+2 -7
src/webapp/features/collections/lib/queries/useCollections.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; 2 + import { getCollections } from '../dal'; 3 3 4 4 interface Props { 5 5 didOrHandle: string; ··· 7 7 } 8 8 9 9 export default function useCollections(props: Props) { 10 - const apiClient = new ApiClient( 11 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 12 - ); 13 - 14 10 const limit = props?.limit ?? 15; 15 11 16 12 return useSuspenseInfiniteQuery({ 17 13 queryKey: ['collections', props.didOrHandle, limit], 18 14 initialPageParam: 1, 19 15 queryFn: ({ pageParam }) => 20 - apiClient.getCollections({ 16 + getCollections(props.didOrHandle, { 21 17 limit, 22 18 page: pageParam, 23 - identifier: props.didOrHandle, 24 19 }), 25 20 getNextPageParam: (lastPage) => { 26 21 return lastPage.pagination.hasMore
+2 -2
src/webapp/features/feeds/containers/myFeedContainer/MyFeedContainer.tsx
··· 1 1 'use client'; 2 2 3 - import useMyFeed from '@/features/feeds/lib/queries/useMyFeed'; 3 + import useGlobalFeed from '@/features/feeds/lib/queries/useGlobalFeed'; 4 4 import FeedItem from '@/features/feeds/components/feedItem/FeedItem'; 5 5 import { Stack, Title, Text, Center, Container } from '@mantine/core'; 6 6 import MyFeedContainerSkeleton from './Skeleton.MyFeedContainer'; ··· 15 15 fetchNextPage, 16 16 hasNextPage, 17 17 isFetchingNextPage, 18 - } = useMyFeed(); 18 + } = useGlobalFeed(); 19 19 20 20 const allActivities = 21 21 data?.pages.flatMap((page) => page.activities ?? []) ?? [];
+17
src/webapp/features/feeds/lib/dal.ts
··· 1 + import { createSembleClient } from '@/services/apiClient'; 2 + import { cache } from 'react'; 3 + 4 + interface PageParams { 5 + page?: number; 6 + limit?: number; 7 + } 8 + 9 + export const getGlobalFeed = cache(async (params?: PageParams) => { 10 + const client = createSembleClient(); 11 + const response = await client.getGlobalFeed({ 12 + page: params?.page, 13 + limit: params?.limit, 14 + }); 15 + 16 + return response; 17 + });
+3 -7
src/webapp/features/feeds/lib/queries/useMyFeed.tsx src/webapp/features/feeds/lib/queries/useGlobalFeed.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; 2 + import { getGlobalFeed } from '../dal'; 3 3 4 4 interface Props { 5 5 limit?: number; 6 6 } 7 7 8 - export default function useMyFeed(props?: Props) { 9 - const apiClient = new ApiClient( 10 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 11 - ); 12 - 8 + export default function useGlobalFeed(props?: Props) { 13 9 const limit = props?.limit ?? 15; 14 10 15 11 const query = useSuspenseInfiniteQuery({ 16 12 queryKey: ['my feed', limit], 17 13 initialPageParam: 1, 18 14 queryFn: ({ pageParam = 1 }) => { 19 - return apiClient.getGlobalFeed({ limit, page: pageParam }); 15 + return getGlobalFeed({ limit, page: pageParam }); 20 16 }, 21 17 getNextPageParam: (lastPage) => { 22 18 if (lastPage.pagination.hasMore) {
+10
src/webapp/features/notes/lib/dal.ts
··· 18 18 return response; 19 19 }, 20 20 ); 21 + 22 + export const updateNoteCard = cache( 23 + async (note: { cardId: string; note: string }) => { 24 + // await verifySession(); 25 + const client = createSembleClient(); 26 + const response = await client.updateNoteCard(note); 27 + 28 + return response; 29 + }, 30 + );
+2 -6
src/webapp/features/notes/lib/mutations/useUpdateNote.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 + import { updateNoteCard } from '../dal'; 3 3 4 4 export default function useUpdateNote() { 5 - const apiClient = new ApiClient( 6 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 7 - ); 8 - 9 5 const queryClient = useQueryClient(); 10 6 11 7 const mutation = useMutation({ 12 8 mutationFn: (note: { cardId: string; note: string }) => { 13 - return apiClient.updateNoteCard(note); 9 + return updateNoteCard(note); 14 10 }, 15 11 16 12 onSuccess: (data) => {
+1
src/webapp/features/profile/lib/dal.ts
··· 11 11 }); 12 12 13 13 export const getMyProfile = cache(async () => { 14 + // await verifySession(); 14 15 const client = createSembleClient(); 15 16 const response = await client.getMyProfile(); 16 17
+2 -6
src/webapp/features/profile/lib/queries/useProfile.tsx
··· 1 - import { ApiClient } from '@/api-client/ApiClient'; 2 1 import { useSuspenseQuery } from '@tanstack/react-query'; 2 + import { getProfile } from '../dal'; 3 3 4 4 interface Props { 5 5 didOrHandle: string; 6 6 } 7 7 8 8 export default function useProfile(props: Props) { 9 - const apiClient = new ApiClient( 10 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000', 11 - ); 12 - 13 9 const profile = useSuspenseQuery({ 14 10 queryKey: ['profile', props.didOrHandle], 15 - queryFn: () => apiClient.getProfile({ identifier: props.didOrHandle }), 11 + queryFn: () => getProfile(props.didOrHandle), 16 12 }); 17 13 18 14 return profile;
+5 -3
src/webapp/hooks/useAuth.tsx
··· 6 6 import type { GetProfileResponse } from '@/api-client/ApiClient'; 7 7 import { ClientCookieAuthService } from '@/services/auth/CookieAuthService.client'; 8 8 9 + const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://127.0.0.1:4000'; 10 + 9 11 interface AuthContextType { 10 12 user: GetProfileResponse | null; 11 13 isAuthenticated: boolean; ··· 33 35 const query = useQuery<GetProfileResponse | null>({ 34 36 queryKey: ['authenticated user'], 35 37 queryFn: async () => { 36 - const response = await fetch('/api/auth/me', { 38 + const response = await fetch(`${appUrl}/api/auth/me`, { 37 39 method: 'GET', 38 40 credentials: 'include', // HttpOnly cookies sent automatically 39 41 }); 40 - 41 42 // unauthenticated 42 43 if (!response.ok) { 43 44 throw new Error('Not authenticated'); 44 45 } 45 46 46 47 const data = await response.json(); 48 + 47 49 return data.user as GetProfileResponse; 48 50 }, 49 51 staleTime: 5 * 60 * 1000, // cache for 5 minutes ··· 52 54 }); 53 55 54 56 useEffect(() => { 55 - if (query.isError) logout(); 57 + if (query.isError && !query.isLoading) logout(); 56 58 }, [query.isError, logout]); 57 59 58 60 const contextValue: AuthContextType = {
+16 -82
src/webapp/lib/auth/dal.ts
··· 1 - import 'server-only'; 2 - 1 + import type { GetProfileResponse } from '@/api-client/ApiClient'; 2 + import { ClientCookieAuthService } from '@/services/auth'; 3 + import { redirect } from 'next/navigation'; 3 4 import { cache } from 'react'; 4 - import { cookies } from 'next/headers'; 5 - import { redirect } from 'next/navigation'; 6 - import { isTokenExpiringSoon } from './token'; 7 5 8 - export const verifySession = cache(async () => { 9 - const cookieStore = await cookies(); 10 - const accessToken = cookieStore.get('accessToken')?.value; 11 - const refreshToken = cookieStore.get('refreshToken')?.value; 6 + const appUrl = process.env.APP_URL || 'http://127.0.0.1:4000'; 12 7 13 - // no session tokens — redirect to login 14 - if (!accessToken && !refreshToken) { 15 - redirect('/login'); 16 - } 8 + export const verifySession = cache( 9 + async (): Promise<GetProfileResponse | null> => { 10 + const response = await fetch(`${appUrl}/api/auth/me`, { 11 + method: 'GET', 12 + credentials: 'include', // HttpOnly cookies sent automatically 13 + }); 17 14 18 - const backendUrl = 19 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000'; 20 - 21 - // token expired or about to expire 22 - if ((!accessToken || isTokenExpiringSoon(accessToken)) && refreshToken) { 23 - const refreshResponse = await fetch( 24 - `${backendUrl}/api/users/oauth/refresh`, 25 - { 26 - method: 'POST', 27 - headers: { 'Content-Type': 'application/json' }, 28 - body: JSON.stringify({ refreshToken }), 29 - cache: 'no-store', 30 - }, 31 - ); 32 - 33 - if (!refreshResponse.ok) { 34 - // clear invalid tokens and redirect to login 35 - cookieStore.delete('accessToken'); 36 - cookieStore.delete('refreshToken'); 15 + if (!response.ok) { 16 + await ClientCookieAuthService.clearTokens(); 37 17 redirect('/login'); 38 18 } 39 19 40 - const newTokens = await refreshResponse.json(); 41 - const newAccessToken = newTokens.accessToken; 42 - 43 - // update cookie 44 - cookieStore.set('accessToken', newAccessToken, { 45 - httpOnly: true, 46 - secure: true, 47 - sameSite: 'lax', 48 - path: '/', 49 - }); 20 + const data = await response.json(); 50 21 51 - return { isAuthenticated: true }; 52 - } 53 - 54 - // if access token is valid 55 - return { isAuthenticated: true }; 56 - }); 57 - 58 - export const getUser = cache(async () => { 59 - const cookieStore = await cookies(); 60 - const accessToken = cookieStore.get('accessToken')?.value; 61 - const refreshToken = cookieStore.get('refreshToken')?.value; 62 - 63 - if (!accessToken && !refreshToken) redirect('/login'); 64 - 65 - const backendUrl = 66 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000'; 67 - 68 - // Forward cookies manually 69 - const cookieHeader = [ 70 - accessToken ? `accessToken=${accessToken}` : null, 71 - refreshToken ? `refreshToken=${refreshToken}` : null, 72 - ] 73 - .filter(Boolean) 74 - .join('; '); 75 - 76 - const res = await fetch(`${backendUrl}/api/users/me`, { 77 - headers: { 78 - 'Content-Type': 'application/json', 79 - Cookie: cookieHeader, // forward the cookies 80 - }, 81 - cache: 'no-store', 82 - }); 83 - 84 - if (!res.ok) { 85 - redirect('/login'); 86 - } 87 - 88 - const user = await res.json(); 89 - return user; 90 - }); 22 + return data.user as GetProfileResponse; 23 + }, 24 + );
+3 -1
src/webapp/services/auth/CookieAuthService.client.ts
··· 1 + const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://127.0.0.1:4000'; 2 + 1 3 export class ClientCookieAuthService { 2 4 // Note: With HttpOnly cookies, we cannot read tokens from document.cookie 3 5 // The browser automatically sends cookies with requests using credentials: 'include' ··· 19 21 // Clear cookies via API (logout) 20 22 static async clearTokens(): Promise<void> { 21 23 try { 22 - const response = await fetch('/api/auth/logout', { 24 + const response = await fetch(`${appUrl}/api/auth/logout`, { 23 25 method: 'POST', 24 26 credentials: 'include', 25 27 });