diff --git a/packages/app/components/dashboard/FeedPreview.tsx b/packages/app/components/dashboard/FeedPreview.tsx deleted file mode 100644 index bdbf24571..000000000 --- a/packages/app/components/dashboard/FeedPreview.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import { RText as OriginalRText, RStack } from '@packrat/ui'; -import { RLink } from '@packrat/ui'; -import { View } from 'react-native'; -import Carousel from '../carousel'; -import useCustomStyles from 'app/hooks/useCustomStyles'; -import { useFeed } from 'app/hooks/feed'; -import loadStyles from './feedpreview.style'; - -interface FeedItem { - id: string; - name: string; - type: string | null; - description: string; -} - -interface FeedPreviewScrollProps { - itemWidth: number; -} -const RText: any = OriginalRText; - -const FeedPreviewScroll: React.FC = ({ itemWidth }) => { - const styles = useCustomStyles(loadStyles); - const { data: feedData } = useFeed(); - - console.log('feedData', feedData) - - return ( - - {feedData?.filter((item): item is FeedItem => item.type !== null).map((item: FeedItem, index: number) => { - const linkStr = `/${item.type}/${item.id}`; - return linkStr ? ( - - - - {item.name} - - {item.type} - - - - {item.description} - - - - ) : null; - })} - - ); -}; - -const FeedPreview: React.FC = () => { - return ; -}; -export default FeedPreview; diff --git a/packages/app/components/feedPreview/FeedPreviewCard.tsx b/packages/app/components/feedPreview/FeedPreviewCard.tsx new file mode 100644 index 000000000..49b976c71 --- /dev/null +++ b/packages/app/components/feedPreview/FeedPreviewCard.tsx @@ -0,0 +1,145 @@ +import { useState } from 'react'; +import { RText as OriginalRText, RStack } from '@packrat/ui'; +import { RLink } from '@packrat/ui'; +import { LayoutChangeEvent, View } from 'react-native'; +import useCustomStyles from 'app/hooks/useCustomStyles'; +import loadStyles from './feedpreview.style'; +import { AntDesign, MaterialIcons } from '@expo/vector-icons'; +import useTheme from 'app/hooks/useTheme'; +import { useItemWeightUnit } from 'app/hooks/items'; +import { convertWeight } from 'app/utils/convertWeight'; +import { formatNumber } from 'app/utils/formatNumber'; +import { hexToRGBA } from 'app/utils/colorFunctions'; + +// TODO FeedItem is one of: trip, pack, similar pack & item +export type FeedItem = any; + +interface FeedPreviewCardProps { + linkStr: string; + item: FeedItem; +} + +const RText: any = OriginalRText; + +const FeedPreviewCard: React.FC = ({ linkStr, item }) => { + const { currentTheme } = useTheme(); + const styles = useCustomStyles(loadStyles); + const [weightUnit] = useItemWeightUnit(); + const formattedWeight = convertWeight(item.total_weight, 'g', weightUnit); + const [cardWidth, setCardWidth] = useState(); + + const handleSetCardWidth = (event: LayoutChangeEvent) => { + const { width } = event.nativeEvent.layout; + setCardWidth(width); + }; + + return ( + + + + + + + + + + {item.name} + + + + {formatNumber(formattedWeight)} + {weightUnit} + + + + + {item.favorites_count} + + + + + + {new Date(item.createdAt).toLocaleString('en-US', { + month: 'short', + day: '2-digit', + ...(new Date(item.createdAt).getFullYear() == + new Date().getFullYear() + ? {} + : { year: 'numeric' }), + })} + + + + Ttl Score: {item.total_score} + + + {/* + {item.description} + */} + + + + ); +}; + +export default FeedPreviewCard; diff --git a/packages/app/components/dashboard/feedpreview.style.tsx b/packages/app/components/feedPreview/feedpreview.style.tsx similarity index 66% rename from packages/app/components/dashboard/feedpreview.style.tsx rename to packages/app/components/feedPreview/feedpreview.style.tsx index 404985156..4240d4e2a 100644 --- a/packages/app/components/dashboard/feedpreview.style.tsx +++ b/packages/app/components/feedPreview/feedpreview.style.tsx @@ -7,11 +7,9 @@ const loadStyles = (theme: any, appTheme: any) => { marginBottom: 20, }, cardStyles: { - height: 100, - width: '100%', backgroundColor: appTheme.colors.primary, - borderRadius: 5, - padding: 20, + borderRadius: 8, + overflow: 'hidden', }, feedItem: { width: 250, @@ -25,16 +23,12 @@ const loadStyles = (theme: any, appTheme: any) => { }, feedItemTitle: { fontWeight: 'bold', - fontSize: 17, - color: currentTheme.colors.text, - marginBottom: 5, - }, - feedItemType: { - fontWeight: 'bold', - fontSize: 16, + fontSize: 18, color: currentTheme.colors.text, - backgroundColor: currentTheme.colors.background, - marginBottom: 5, + marginBottom: 12, + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', }, }; }; diff --git a/packages/app/components/feedPreview/index.tsx b/packages/app/components/feedPreview/index.tsx new file mode 100644 index 000000000..d4bd8fdbf --- /dev/null +++ b/packages/app/components/feedPreview/index.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import Carousel from '../carousel'; +import { useFeed } from 'app/hooks/feed'; +import { default as FeedPreviewCard, type FeedItem } from './FeedPreviewCard'; + +interface FeedPreviewScrollProps { + itemWidth: number; + feedType: string; + id?: string; +} + +const FeedPreviewScroll: React.FC = ({ + itemWidth, + feedType, + id, +}) => { + const { data: feedData } = useFeed({ feedType, id }); + + return ( + + {feedData + ?.filter((item): item is FeedItem => item.type !== null) + .map((item: FeedItem) => { + const linkStr = `/${item.type}/${item.id}`; + return linkStr ? ( + + ) : null; + })} + + ); +}; + +const FeedPreview: React.FC<{ feedType: string; id?: string }> = ({ + feedType, + id, +}) => { + return ; +}; +export default FeedPreview; diff --git a/packages/app/components/layout/Layout.tsx b/packages/app/components/layout/Layout.tsx index e5ac9c968..2a2a89f28 100644 --- a/packages/app/components/layout/Layout.tsx +++ b/packages/app/components/layout/Layout.tsx @@ -1,17 +1,27 @@ +import { StyleProp, ViewStyle } from 'react-native'; import { View } from 'react-native'; -const Layout = ({ children }) => { +const Layout = ({ + children, + customStyle = {}, +}: { + children: React.ReactNode; + customStyle?: StyleProp; +}) => { return ( {children} diff --git a/packages/app/components/pack/PackDetails.tsx b/packages/app/components/pack/PackDetails.tsx index c8c578e7e..cd1659304 100644 --- a/packages/app/components/pack/PackDetails.tsx +++ b/packages/app/components/pack/PackDetails.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { CLIENT_URL } from '@packrat/config'; -import { RText } from '@packrat/ui'; +import { RH3, RText } from '@packrat/ui'; import { useAuthUser } from 'app/auth/hooks'; import Layout from 'app/components/layout/Layout'; import { useIsAuthUserPack } from 'app/hooks/packs/useIsAuthUserPack'; @@ -16,6 +16,9 @@ import { DetailsComponent } from '../details'; import { TableContainer } from '../pack_table/Table'; import { AddItemModal } from './AddItemModal'; import { ImportItemModal } from './ImportItemModal'; +import FeedPreview from 'app/components/feedPreview'; +import LargeCard from 'app/components/card/LargeCard'; +import useTheme from 'app/hooks/useTheme'; const SECTION = { TABLE: 'TABLE', @@ -25,6 +28,7 @@ const SECTION = { }; export function PackDetails() { + const { currentTheme } = useTheme(); // const [canCopy, setCanCopy] = useParam('canCopy') const canCopy = false; const [packId] = usePackId(); @@ -54,7 +58,7 @@ export function PackDetails() { if (isLoading) return Loading...; return ( - + {!isError && ( + + + + Similar Packs + + + )} {/* Disable Chat */} diff --git a/packages/app/hooks/feed/index.ts b/packages/app/hooks/feed/index.ts index ba3d8a9e5..f588cb888 100644 --- a/packages/app/hooks/feed/index.ts +++ b/packages/app/hooks/feed/index.ts @@ -1,13 +1,21 @@ import { usePublicFeed } from './publicFeed'; import { useUserPacks } from './../packs'; import { useUserTrips } from '../singletrips'; +import { useSimilarPacks } from 'app/hooks/packs/useSimilarPacks'; -export const useFeed = ( +export const useFeed = ({ queryString = 'Most Recent', - ownerId: string | undefined = undefined, + ownerId, feedType = 'public', selectedTypes = { pack: true, trip: true }, -) => { + id, +}: Partial<{ + queryString: string; + ownerId: string; + feedType: string; + selectedTypes: Object; + id: string; +}> = {}) => { switch (feedType) { case 'public': return usePublicFeed(queryString, selectedTypes); @@ -15,6 +23,8 @@ export const useFeed = ( return useUserPacks(ownerId || undefined, queryString); case 'userTrips': return useUserTrips(ownerId || undefined); + case 'similarPacks': + return useSimilarPacks(id); default: return { data: null, error: null, isLoading: true }; } diff --git a/packages/app/hooks/packs/index.ts b/packages/app/hooks/packs/index.ts index efc5f1cc0..65f867d44 100644 --- a/packages/app/hooks/packs/index.ts +++ b/packages/app/hooks/packs/index.ts @@ -5,3 +5,4 @@ export * from './useDeletePack'; export * from './useEditPack'; export * from './usePackId'; export * from './useUserPackById'; +export * from './useSimilarPacks'; diff --git a/packages/app/hooks/packs/useSimilarPacks.ts b/packages/app/hooks/packs/useSimilarPacks.ts new file mode 100644 index 000000000..ac27f4a64 --- /dev/null +++ b/packages/app/hooks/packs/useSimilarPacks.ts @@ -0,0 +1,13 @@ +import { queryTrpc } from '../../trpc'; + +export const useSimilarPacks = (id: string) => { + const { data, error, isLoading, refetch } = + queryTrpc.getSimilarPacks.useQuery( + { id, limit: 10 }, + { + refetchOnWindowFocus: true, + }, + ); + + return { data, error, isLoading, refetch }; +}; diff --git a/packages/app/screens/dashboard/index.tsx b/packages/app/screens/dashboard/index.tsx index 13b560ed4..fcf128ec7 100644 --- a/packages/app/screens/dashboard/index.tsx +++ b/packages/app/screens/dashboard/index.tsx @@ -3,7 +3,7 @@ import { Platform, View } from 'react-native'; import { RStack, RScrollView } from '@packrat/ui'; import HeroBanner from '../../components/dashboard/HeroBanner'; import QuickActionsSection from '../../components/dashboard/QuickActionSection'; -import FeedPreview from '../../components/dashboard/FeedPreview'; +import FeedPreview from '../../components/feedPreview'; import Section from '../../components/dashboard/Section'; import SectionHeader from '../../components/dashboard/SectionHeader'; import useCustomStyles from 'app/hooks/useCustomStyles'; @@ -35,7 +35,7 @@ const Dashboard = () => {
- +
diff --git a/packages/app/screens/feed/Feed.tsx b/packages/app/screens/feed/Feed.tsx index 3e7657dbc..ff6668166 100644 --- a/packages/app/screens/feed/Feed.tsx +++ b/packages/app/screens/feed/Feed.tsx @@ -63,12 +63,12 @@ const Feed = ({ feedType = 'public' }: FeedProps) => { const ownerId = user?.id; const styles = useCustomStyles(loadStyles); - const { data, error, isLoading, refetch } = useFeed( + const { data, error, isLoading, refetch } = useFeed({ queryString, ownerId, feedType, selectedTypes, - ) as UseFeedResult; + }) as UseFeedResult; const onRefresh = () => { setRefreshing(true); diff --git a/server/src/drizzle/methods/pack.ts b/server/src/drizzle/methods/pack.ts index 1d8520c4f..d5e12be55 100644 --- a/server/src/drizzle/methods/pack.ts +++ b/server/src/drizzle/methods/pack.ts @@ -1,7 +1,8 @@ -import { eq, sql, asc, desc, and } from 'drizzle-orm'; +import { eq, sql, asc, desc, and, inArray } from 'drizzle-orm'; import { DbClient } from '../../db/client'; import { type InsertPack, pack as PackTable, itemPacks } from '../../db/schema'; import { convertWeight } from '../../utils/convertWeight'; +import { SQLiteColumn } from 'drizzle-orm/sqlite-core'; export class Pack { getRelations({ includeRelated, ownerId = true, completeItems = false }) { @@ -176,18 +177,33 @@ export class Pack { }); return (await packs).map((pack: any) => ({ ...pack, - scores: JSON.parse(pack.scores as string), - grades: JSON.parse(pack.grades as string), - total_weight: this.computeTotalWeight(pack), - favorites_count: this.computeFavouritesCount(pack), - total_score: this.computeTotalScores(pack), - items: pack.itemPacks.map((itemPack) => itemPack.item), + ...this.computeDerivedProperties(pack), })); } catch (error) { throw new Error(`Failed to fetch packs: ${error.message}`); } } + async findInArray({ + column = PackTable.id, + array, + }: { + column?: SQLiteColumn; + array: string[]; + }) { + const packs = await DbClient.instance.query.pack.findMany({ + where: inArray(column, array), + ...this.getRelations({ + includeRelated: true, + completeItems: true, + }), + }); + return packs.map((pack) => ({ + ...pack, + ...this.computeDerivedProperties(pack), + })); + } + async sortPacksByItems(options: any) { try { const { queryBy, sortItems, is_public, ownerId } = options; @@ -254,6 +270,17 @@ export class Pack { // } // } + computeDerivedProperties(pack) { + return { + scores: JSON.parse(pack.scores as string), + grades: JSON.parse(pack.grades as string), + total_weight: this.computeTotalWeight(pack), + favorites_count: this.computeFavouritesCount(pack), + total_score: this.computeTotalScores(pack), + items: pack.itemPacks?.map((itemPack) => itemPack.item) ?? [], + }; + } + computeTotalWeight(pack) { if (pack.itemPacks && pack.itemPacks.length > 0) { const totalWeight = pack.itemPacks.reduce( diff --git a/server/src/services/pack/getSimilarPacksService.ts b/server/src/services/pack/getSimilarPacksService.ts index 62b559b06..2ef2c78a0 100644 --- a/server/src/services/pack/getSimilarPacksService.ts +++ b/server/src/services/pack/getSimilarPacksService.ts @@ -1,8 +1,5 @@ -import { DbClient } from '../../db/client'; import { Pack } from '../../drizzle/methods/pack'; import { VectorClient } from '../../vector/client'; -import { pack as PacksTable } from '../../db/schema'; -import { inArray } from 'drizzle-orm'; import { PackAndItemVisibilityFilter } from '@packrat/shared-types'; /** @@ -44,15 +41,9 @@ export async function getSimilarPacksService( return []; } - const similarPacksResult = await DbClient.instance - .select() - .from(PacksTable) - .where( - inArray( - PacksTable.id, - matches.map((m) => m.id), - ), - ); + const similarPacksResult = await packClass.findInArray({ + array: matches.map((m) => m.id), + }); // add similarity score to packs result const similarPacks = matches.map((match) => { diff --git a/server/src/tests/routes/pack.spec.ts b/server/src/tests/routes/pack.spec.ts index dd7474f00..290e62ff9 100644 --- a/server/src/tests/routes/pack.spec.ts +++ b/server/src/tests/routes/pack.spec.ts @@ -191,7 +191,10 @@ describe('Pack routes', () => { it('should return similar packs', async () => { expect(similarPacks).toEqual([ - { ...pack, id: packId, similarityScore: 0.89519173 }, + expect.objectContaining({ + id: packId, + similarityScore: 0.89519173, + }), ]); }); });