From 08603d67df8348bf945c79f40876a3c91ee6fe6e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:52:41 +0000 Subject: [PATCH 01/42] Initial plan From 65448efc9fc62aa9532f3a37b51553efeebfc292 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:05:53 +0000 Subject: [PATCH 02/42] Add user profile navigation: routes, components, and GraphQL updates Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../sharer-information/sharer-information.tsx | 39 +++------- .../user-profile-view.container.graphql | 56 ++++++++++++++ .../user-profile-view.container.tsx | 73 +++++++++++++++++++ .../src/components/layouts/home/index.tsx | 2 + .../messages/components/listing-banner.tsx | 9 ++- .../components/my-listings-dashboard.types.ts | 1 + .../requests-table.container.graphql | 1 + .../my-listings/components/requests-table.tsx | 16 +++- .../home/pages/view-user-profile-page.tsx | 5 ++ .../src/components/shared/user-avatar.tsx | 73 +++++++++++++++++++ .../components/shared/user-profile-link.tsx | 26 +++++++ .../schema/types/listing/item-listing.graphql | 1 + .../reservation-request.resolvers.ts | 4 +- 13 files changed, 268 insertions(+), 38 deletions(-) create mode 100644 apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.graphql create mode 100644 apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx create mode 100644 apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.tsx create mode 100644 apps/ui-sharethrift/src/components/shared/user-avatar.tsx create mode 100644 apps/ui-sharethrift/src/components/shared/user-profile-link.tsx diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx index c2f03dd4a..c48ea08d6 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx @@ -1,4 +1,4 @@ -import { Button, Avatar, Row, Col } from 'antd'; +import { Button, Row, Col } from 'antd'; import { useEffect, useState } from 'react'; import { MessageOutlined } from '@ant-design/icons'; import { useMutation } from '@apollo/client/react'; @@ -8,6 +8,8 @@ import type { CreateConversationMutation, CreateConversationMutationVariables, } from '../../../../../../generated.tsx'; +import { UserAvatar } from '../../../../shared/user-avatar.tsx'; +import { UserProfileLink } from '../../../../shared/user-profile-link.tsx'; export type Sharer = { id: string; @@ -100,40 +102,17 @@ export const SharerInformation: React.FC = ({ justifyContent: 'flex-start', }} > - - Sharethrift Logo - - - } - > - {sharer.name.charAt(0).toUpperCase()} - + avatarUrl={sharer.avatar} + />

- {sharer.name} +

shared {sharedTimeAgo} diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.graphql b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.graphql new file mode 100644 index 000000000..32195e207 --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.graphql @@ -0,0 +1,56 @@ +query HomeViewUserProfileContainerUserById($userId: ObjectID!) { + userById(id: $userId) { + ... on PersonalUser { + id + userType + createdAt + account { + ...PersonalUserPublicProfileFields + } + } + ... on AdminUser { + id + userType + createdAt + account { + ...AdminUserPublicProfileFields + } + } + } +} + +fragment PersonalUserPublicProfileFields on PersonalUserAccount { + accountType + username + profile { + ...PersonalUserPublicProfileFieldsProfile + } +} + +fragment PersonalUserPublicProfileFieldsProfile on PersonalUserAccountProfile { + firstName + lastName + aboutMe + location { + city + state + } +} + +fragment AdminUserPublicProfileFields on AdminUserAccount { + accountType + username + profile { + ...AdminUserPublicProfileFieldsProfile + } +} + +fragment AdminUserPublicProfileFieldsProfile on AdminUserAccountProfile { + firstName + lastName + aboutMe + location { + city + state + } +} diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx new file mode 100644 index 000000000..c6ab0fcea --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx @@ -0,0 +1,73 @@ +import { useParams, useNavigate } from 'react-router-dom'; +import { ProfileView } from '../../account/profile/components/profile-view.tsx'; +import { useQuery } from '@apollo/client/react'; +import { ComponentQueryLoader } from '@sthrift/ui-components'; +import { + type ItemListing, + HomeViewUserProfileContainerUserByIdDocument, +} from '../../../../../../generated.tsx'; + +export const UserProfileViewContainer: React.FC = () => { + const { userId } = useParams<{ userId: string }>(); + const navigate = useNavigate(); + + const { + data: userQueryData, + loading: userLoading, + error: userError, + } = useQuery(HomeViewUserProfileContainerUserByIdDocument, { + variables: { userId: userId ?? '' }, + skip: !userId, + }); + + const handleListingClick = (listingId: string) => { + navigate(`/listing/${listingId}`); + }; + + const viewedUser = userQueryData?.userById; + + if (!userId) { + return

User ID is required
; + } + + if (!viewedUser) { + return null; + } + + const { account, createdAt } = viewedUser; + + const profileUser = { + id: viewedUser.id, + firstName: account?.profile?.firstName || '', + lastName: account?.profile?.lastName || '', + username: account?.username || '', + email: '', // Don't expose email for other users + accountType: account?.accountType || '', + location: { + city: account?.profile?.location?.city || '', + state: account?.profile?.location?.state || '', + }, + createdAt: createdAt || '', + }; + + // For viewing other users' profiles, we don't show their listings + // This would require a backend query to fetch public listings by user ID + const listings: ItemListing[] = []; + + return ( + {}} + onListingClick={handleListingClick} + /> + } + /> + ); +}; diff --git a/apps/ui-sharethrift/src/components/layouts/home/index.tsx b/apps/ui-sharethrift/src/components/layouts/home/index.tsx index 810d52f08..4618b72f1 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/index.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/index.tsx @@ -6,6 +6,7 @@ import { MyReservationsRoutes } from './my-reservations/Index.tsx'; import { Listings } from './pages/all-listings-page.tsx'; import { ViewListing } from './pages/view-listing-page.tsx'; import { CreateListing } from './pages/create-listing-page.tsx'; +import { ViewUserProfile } from './pages/view-user-profile-page.tsx'; import { HomeTabsLayout } from './section-layout.tsx'; import { AdminDashboardMain } from './account/admin-dashboard/pages/admin-dashboard-main.tsx'; @@ -16,6 +17,7 @@ export const HomeRoutes: React.FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/listing-banner.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/listing-banner.tsx index ac400e08b..d92f1c027 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/listing-banner.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/listing-banner.tsx @@ -2,6 +2,7 @@ import { Card, Typography, Avatar, Tag, Row, Col } from "antd"; import { SwapOutlined, AppstoreAddOutlined } from "@ant-design/icons"; import bikeListingImg from "@sthrift/ui-components/src/assets/item-images/bike-listing.png"; import type { User } from "../../../../../generated.tsx"; +import { UserProfileLink } from "../../../../shared/user-profile-link.tsx"; const imgRectangle26 = bikeListingImg; @@ -70,7 +71,9 @@ export const ListingBanner: React.FC = (props) => { marginTop: 4, }} > - = (props) => { color: "var(--color-primary)", lineHeight: "20px", }} - > - {firstName} - + /> diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts index 2894f3ae6..816499e29 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts @@ -13,6 +13,7 @@ export interface ListingRequestData { title: string; image?: string | null; requestedBy: string; + requestedById?: string | null; requestedOn: string; reservationPeriod: string; status: string; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.container.graphql b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.container.graphql index 72dd5f98b..352b7f773 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.container.graphql +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.container.graphql @@ -28,6 +28,7 @@ fragment HomeRequestsTableContainerRequestFields on ListingRequest { title image requestedBy + requestedById requestedOn reservationPeriod status diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx index 13c206b37..9509e02f3 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx @@ -8,6 +8,7 @@ import { getStatusTagClass, getActionButtons, } from './requests-status-helpers.tsx'; +import { UserProfileLink } from '../../../../shared/user-profile-link.tsx'; const { Search } = Input; @@ -107,9 +108,18 @@ export const RequestsTable: React.FC = ({ key: 'requestedBy', sorter: true, sortOrder: sorter.field === 'requestedBy' ? sorter.order : null, - render: (username: string) => ( - {username} - ), + render: (username: string, record: ListingRequestData) => { + if (record.requestedById) { + return ( + + ); + } + return {username}; + }, }, { title: 'Requested On', diff --git a/apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.tsx b/apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.tsx new file mode 100644 index 000000000..fdfefc109 --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.tsx @@ -0,0 +1,5 @@ +import { UserProfileViewContainer } from '../components/view-user-profile/user-profile-view.container.tsx'; + +export const ViewUserProfile: React.FC = () => { + return ; +}; diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx new file mode 100644 index 000000000..6c671f350 --- /dev/null +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx @@ -0,0 +1,73 @@ +import { Avatar as AntAvatar } from 'antd'; +import { Link } from 'react-router-dom'; + +export interface UserAvatarProps { + userId: string; + userName: string; + size?: number; + avatarUrl?: string; + className?: string; + style?: React.CSSProperties; + shape?: 'circle' | 'square'; +} + +export const UserAvatar: React.FC = ({ + userId, + userName, + size = 48, + avatarUrl, + className = '', + style = {}, + shape = 'circle', +}) => { + const avatarContent = avatarUrl ? ( + + ) : ( + + Sharethrift Logo + + + } + > + {userName.charAt(0).toUpperCase()} + + ); + + return ( + + {avatarContent} + + ); +}; diff --git a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx new file mode 100644 index 000000000..521c856f6 --- /dev/null +++ b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx @@ -0,0 +1,26 @@ +import { Typography } from 'antd'; +import { Link } from 'react-router-dom'; + +const { Link: AntLink } = Typography; + +export interface UserProfileLinkProps { + userId: string; + displayName: string; + className?: string; + style?: React.CSSProperties; +} + +export const UserProfileLink: React.FC = ({ + userId, + displayName, + className = '', + style = {}, +}) => { + return ( + + + {displayName} + + + ); +}; diff --git a/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql b/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql index 60b7d154d..0b2a09276 100644 --- a/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql +++ b/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql @@ -48,6 +48,7 @@ type ListingRequest { title: String! image: String requestedBy: String! + requestedById: ObjectID requestedOn: String! reservationPeriod: String! status: String! diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts index 59a0cdac7..2a457d7dc 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts @@ -13,7 +13,7 @@ interface ListingRequestDomainShape { reservationPeriodStart?: Date; reservationPeriodEnd?: Date; listing?: { title?: string; [k: string]: unknown }; - reserver?: { account?: { username?: string } }; + reserver?: { id?: string; account?: { username?: string } }; [k: string]: unknown; // allow passthrough } @@ -22,6 +22,7 @@ interface ListingRequestUiShape { title: string; image: string; requestedBy: string; + requestedById?: string | null; requestedOn: string; reservationPeriod: string; status: string; @@ -58,6 +59,7 @@ function paginateAndFilterListingRequests( requestedBy: r.reserver?.account?.username ? `@${r.reserver.account.username}` : '@unknown', + requestedById: r.reserver?.id ?? null, requestedOn: r.createdAt instanceof Date ? r.createdAt.toISOString() From 421e2be6f99fd54340fe36719f7f79ba3889c419 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:10:50 +0000 Subject: [PATCH 03/42] Update message thread with clickable avatars and add Storybook stories Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../messages/components/message-thread.tsx | 33 +------ .../components/shared/user-avatar.stories.tsx | 98 +++++++++++++++++++ .../shared/user-profile-link.stories.tsx | 69 +++++++++++++ 3 files changed, 172 insertions(+), 28 deletions(-) create mode 100644 apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx create mode 100644 apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx index b27f2b069..b57d34c45 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx @@ -9,6 +9,7 @@ import { } from "antd"; import { SendOutlined } from "@ant-design/icons"; import { useRef } from "react"; +import { UserAvatar } from "../../../shared/user-avatar.tsx"; interface Message { id: string; @@ -193,35 +194,11 @@ function MessageBubble({ message, isOwn, showAvatar }: MessageBubbleProps) { }} > {showAvatar && !isOwn && ( - - Sharethrift Logo - - - } - > - {message.authorId.charAt(0).toUpperCase()} - + /> )} {showAvatar && isOwn &&
} {!showAvatar &&
} diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx new file mode 100644 index 000000000..071b49ee2 --- /dev/null +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx @@ -0,0 +1,98 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { BrowserRouter } from 'react-router-dom'; +import { UserAvatar } from './user-avatar.tsx'; + +const meta: Meta = { + title: 'Shared/UserAvatar', + component: UserAvatar, + decorators: [ + (Story) => ( + + + + ), + ], + tags: ['autodocs'], + argTypes: { + userId: { + control: 'text', + description: 'The ID of the user to link to', + }, + userName: { + control: 'text', + description: 'The name of the user for accessibility and initial', + }, + size: { + control: 'number', + description: 'Size of the avatar in pixels', + }, + avatarUrl: { + control: 'text', + description: 'URL of the user avatar image (optional)', + }, + shape: { + control: 'select', + options: ['circle', 'square'], + description: 'Shape of the avatar', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + userId: '507f1f77bcf86cd799439011', + userName: 'John Doe', + size: 48, + }, +}; + +export const Small: Story = { + args: { + userId: '507f1f77bcf86cd799439011', + userName: 'Jane Smith', + size: 32, + }, +}; + +export const Large: Story = { + args: { + userId: '507f1f77bcf86cd799439011', + userName: 'Alice Johnson', + size: 72, + }, +}; + +export const Square: Story = { + args: { + userId: '507f1f77bcf86cd799439011', + userName: 'Bob Williams', + size: 48, + shape: 'square', + }, +}; + +export const InContext: Story = { + render: () => ( +
+ +
+

Alice Johnson

+

Click the avatar to view profile

+
+
+ ), +}; + +export const MultipleAvatars: Story = { + render: () => ( +
+ + + + +
+ ), +}; diff --git a/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx b/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx new file mode 100644 index 000000000..c4859f602 --- /dev/null +++ b/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx @@ -0,0 +1,69 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { BrowserRouter } from 'react-router-dom'; +import { UserProfileLink } from './user-profile-link.tsx'; + +const meta: Meta = { + title: 'Shared/UserProfileLink', + component: UserProfileLink, + decorators: [ + (Story) => ( + + + + ), + ], + tags: ['autodocs'], + argTypes: { + userId: { + control: 'text', + description: 'The ID of the user to link to', + }, + displayName: { + control: 'text', + description: 'The display name to show as the link text', + }, + className: { + control: 'text', + description: 'Additional CSS class names', + }, + style: { + control: 'object', + description: 'Custom inline styles', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + userId: '507f1f77bcf86cd799439011', + displayName: 'John Doe', + }, +}; + +export const WithCustomStyle: Story = { + args: { + userId: '507f1f77bcf86cd799439011', + displayName: 'Jane Smith', + style: { + fontSize: '18px', + fontWeight: 'bold', + color: '#ff0000', + }, + }, +}; + +export const InContext: Story = { + render: () => ( +
+

+ This listing is shared by{' '} + {' '} + and was requested by{' '} + . +

+
+ ), +}; From 9c7110ffdfd8e75f71da6e985504b42d2a0fc68c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 20:12:26 +0000 Subject: [PATCH 04/42] Add documentation for user profile navigation pattern Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../docs/user-profile-navigation.md | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 apps/ui-sharethrift/docs/user-profile-navigation.md diff --git a/apps/ui-sharethrift/docs/user-profile-navigation.md b/apps/ui-sharethrift/docs/user-profile-navigation.md new file mode 100644 index 000000000..fdbb49a19 --- /dev/null +++ b/apps/ui-sharethrift/docs/user-profile-navigation.md @@ -0,0 +1,178 @@ +# User Profile Navigation + +This document describes the navigation pattern for linking to user profiles from various locations in the ShareThrift UI. + +## Overview + +Users can navigate to another user's profile by clicking on their name or profile image wherever it appears in the application. This provides a consistent and intuitive way to view user information. + +## Navigation Route + +User profiles are accessed via the route: + +``` +/user/:userId +``` + +Where `:userId` is the ObjectID of the user. + +## Shared Components + +Two reusable components are provided for consistent user profile navigation: + +### UserProfileLink + +A clickable text link that navigates to a user's profile. + +**Usage:** +```tsx +import { UserProfileLink } from '../shared/user-profile-link.tsx'; + + +``` + +**Props:** +- `userId` (required): The ObjectID of the user +- `displayName` (required): The text to display as the link +- `className` (optional): Additional CSS classes +- `style` (optional): Custom inline styles + +### UserAvatar + +A clickable avatar that navigates to a user's profile. + +**Usage:** +```tsx +import { UserAvatar } from '../shared/user-avatar.tsx'; + + +``` + +**Props:** +- `userId` (required): The ObjectID of the user +- `userName` (required): The name of the user (for accessibility and fallback initial) +- `size` (optional): Size of the avatar in pixels (default: 48) +- `avatarUrl` (optional): URL of the user's avatar image +- `className` (optional): Additional CSS classes +- `style` (optional): Custom inline styles +- `shape` (optional): 'circle' or 'square' (default: 'circle') + +## Implementation Locations + +User profile navigation has been implemented in the following components: + +### 1. Listing Sharer Information +**Component:** `sharer-information.tsx` +- Avatar and name link to the sharer's profile +- Located on item listing detail pages + +### 2. Conversation Headers (Listing Banner) +**Component:** `listing-banner.tsx` +- User name in conversation headers links to their profile +- Located in the Messages section + +### 3. My Listings - Requests Table +**Component:** `requests-table.tsx` +- "Requested By" column entries link to the requester's profile +- Located in My Listings > Requests view + +### 4. Message Thread +**Component:** `message-thread.tsx` +- Message author avatars link to their profiles +- Located in the Messages conversation view + +## GraphQL Integration + +### User Profile Query + +The `UserProfileViewContainer` uses the following GraphQL query to fetch user data: + +```graphql +query HomeViewUserProfileContainerUserById($userId: ObjectID!) { + userById(id: $userId) { + ... on PersonalUser { + id + userType + createdAt + account { + accountType + username + profile { + firstName + lastName + aboutMe + location { + city + state + } + } + } + } + ... on AdminUser { + # Similar fields for admin users + } + } +} +``` + +### Listing Request Enhancement + +The `ListingRequest` type has been enhanced with a `requestedById` field: + +```graphql +type ListingRequest { + id: ObjectID! + title: String! + image: String + requestedBy: String! + requestedById: ObjectID # New field + requestedOn: String! + reservationPeriod: String! + status: String! +} +``` + +## Accessibility + +Both `UserProfileLink` and `UserAvatar` components are designed with accessibility in mind: + +- Use semantic Ant Design Link component for proper keyboard navigation +- Include ARIA labels on avatars (`aria-label="View {userName}'s profile"`) +- Maintain focus states for keyboard navigation +- Provide visual indication of clickability (hover states, pointer cursor) + +## Best Practices + +1. **Always use the shared components** (`UserProfileLink` and `UserAvatar`) instead of creating custom navigation implementations +2. **Pass the user ID** from your GraphQL data to ensure correct navigation +3. **Provide meaningful display names** for accessibility +4. **Test keyboard navigation** when implementing in new locations +5. **Handle edge cases** (e.g., when user data is unavailable) + +## Testing + +Storybook stories are available for both components: +- `UserProfileLink.stories.tsx` +- `UserAvatar.stories.tsx` + +These stories demonstrate various use cases and styling options. + +## Future Enhancements + +Potential future enhancements to the user profile navigation pattern: + +1. Add user listings to the public profile view (requires backend query for public listings by user ID) +2. Implement user blocking/reporting functionality accessible from profile +3. Add user rating/review system +4. Enable direct messaging from profile view +5. Show mutual connections or shared listings From 7ab425016978d6d9fc53fe756a1fd978ada8abc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:02:24 +0000 Subject: [PATCH 05/42] fix: address code review feedback for user profile navigation components Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../user-profile-view.container.tsx | 43 ++++++++++--------- .../components/shared/user-avatar.stories.tsx | 15 +++++++ .../src/components/shared/user-avatar.tsx | 12 +++++- .../shared/user-profile-link.stories.tsx | 14 ++++++ .../components/shared/user-profile-link.tsx | 28 +++++++++--- 5 files changed, 82 insertions(+), 30 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx index c6ab0fcea..5efa8e318 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx @@ -24,44 +24,45 @@ export const UserProfileViewContainer: React.FC = () => { navigate(`/listing/${listingId}`); }; - const viewedUser = userQueryData?.userById; - if (!userId) { return
User ID is required
; } - if (!viewedUser) { - return null; - } - - const { account, createdAt } = viewedUser; + const viewedUser = userQueryData?.userById; - const profileUser = { - id: viewedUser.id, - firstName: account?.profile?.firstName || '', - lastName: account?.profile?.lastName || '', - username: account?.username || '', - email: '', // Don't expose email for other users - accountType: account?.accountType || '', - location: { - city: account?.profile?.location?.city || '', - state: account?.profile?.location?.state || '', - }, - createdAt: createdAt || '', + const buildProfileUser = () => { + if (!viewedUser) return null; + + const { account, createdAt } = viewedUser; + return { + id: viewedUser.id, + firstName: account?.profile?.firstName || '', + lastName: account?.profile?.lastName || '', + username: account?.username || '', + email: '', // Don't expose email for other users + accountType: account?.accountType || '', + location: { + city: account?.profile?.location?.city || '', + state: account?.profile?.location?.state || '', + }, + createdAt: createdAt || '', + }; }; // For viewing other users' profiles, we don't show their listings // This would require a backend query to fetch public listings by user ID const listings: ItemListing[] = []; + const profileUser = buildProfileUser(); + return ( {}} diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx index 071b49ee2..fbaa654c5 100644 --- a/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx @@ -74,6 +74,21 @@ export const Square: Story = { }, }; +export const WithoutUserId: Story = { + args: { + userId: '', + userName: 'Unknown User', + size: 48, + }, + parameters: { + docs: { + description: { + story: 'When no userId is provided, the avatar is rendered without a link (non-clickable).', + }, + }, + }, +}; + export const InContext: Story = { render: () => (
diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx index 6c671f350..cb5973f0d 100644 --- a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx @@ -20,13 +20,16 @@ export const UserAvatar: React.FC = ({ style = {}, shape = 'circle', }) => { + // Determine if we have a valid userId for linking (non-empty and non-whitespace) + const isClickable = !!userId && userId.trim() !== ''; + const avatarContent = avatarUrl ? ( ) : ( @@ -41,7 +44,7 @@ export const UserAvatar: React.FC = ({ justifyContent: 'center', flexShrink: 0, fontFamily: 'var(--Urbanist, Arial, sans-serif)', - cursor: 'pointer', + cursor: isClickable ? 'pointer' : 'default', ...style, }} icon={ @@ -65,6 +68,11 @@ export const UserAvatar: React.FC = ({ ); + // If no valid userId, render avatar without link + if (!isClickable) { + return avatarContent; + } + return ( {avatarContent} diff --git a/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx b/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx index c4859f602..137cf7b4d 100644 --- a/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx @@ -55,6 +55,20 @@ export const WithCustomStyle: Story = { }, }; +export const WithoutUserId: Story = { + args: { + userId: '', + displayName: 'Unknown User', + }, + parameters: { + docs: { + description: { + story: 'When no userId is provided, the component renders as plain text instead of a link.', + }, + }, + }, +}; + export const InContext: Story = { render: () => (
diff --git a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx index 521c856f6..0d0e5c9de 100644 --- a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx @@ -1,8 +1,5 @@ -import { Typography } from 'antd'; import { Link } from 'react-router-dom'; -const { Link: AntLink } = Typography; - export interface UserProfileLinkProps { userId: string; displayName: string; @@ -16,11 +13,28 @@ export const UserProfileLink: React.FC = ({ className = '', style = {}, }) => { - return ( - - + // If no valid userId (empty or whitespace-only), render as plain text instead of a broken link + const isValidUserId = !!userId && userId.trim() !== ''; + + if (!isValidUserId) { + return ( + {displayName} - + + ); + } + + return ( + + {displayName} ); }; From 455f446276794ac22d0516e8215ed512875d0433 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Fri, 28 Nov 2025 22:44:43 +0530 Subject: [PATCH 06/42] feat: add user profile navigation and improve component imports --- .../sharer-information/sharer-information.tsx | 250 +++++++++--------- .../user-profile-view.container.tsx | 124 ++++----- .../src/components/layouts/home/index.tsx | 1 + .../messages/components/message-thread.tsx | 18 +- 4 files changed, 194 insertions(+), 199 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx index c48ea08d6..0b4345ae7 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx @@ -1,137 +1,143 @@ -import { Button, Row, Col } from 'antd'; -import { useEffect, useState } from 'react'; -import { MessageOutlined } from '@ant-design/icons'; -import { useMutation } from '@apollo/client/react'; -import { useNavigate } from 'react-router-dom'; -import { CreateConversationDocument, HomeConversationListContainerConversationsByUserDocument } from '../../../../../../generated.tsx'; +import { Button, Row, Col } from "antd"; +import { useEffect, useState } from "react"; +import { MessageOutlined } from "@ant-design/icons"; +import { useMutation } from "@apollo/client/react"; +import { useNavigate } from "react-router-dom"; +import { + CreateConversationDocument, + HomeConversationListContainerConversationsByUserDocument, +} from "../../../../../../generated.tsx"; import type { - CreateConversationMutation, - CreateConversationMutationVariables, -} from '../../../../../../generated.tsx'; -import { UserAvatar } from '../../../../shared/user-avatar.tsx'; -import { UserProfileLink } from '../../../../shared/user-profile-link.tsx'; + CreateConversationMutation, + CreateConversationMutationVariables, +} from "../../../../../../generated.tsx"; +import { UserAvatar } from "../../../../../shared/user-avatar.tsx"; +import { UserProfileLink } from "../../../../../shared/user-profile-link.tsx"; export type Sharer = { - id: string; - name: string; - avatar?: string; + id: string; + name: string; + avatar?: string; }; export interface SharerInformationProps { - sharer: Sharer; - listingId: string; - isOwner?: boolean; - sharedTimeAgo?: string; - className?: string; - currentUserId?: string | null; + sharer: Sharer; + listingId: string; + isOwner?: boolean; + sharedTimeAgo?: string; + className?: string; + currentUserId?: string | null; } export const SharerInformation: React.FC = ({ - sharer, - listingId, - isOwner = false, - sharedTimeAgo = '2 days ago', - className = '', - currentUserId, + sharer, + listingId, + isOwner = false, + sharedTimeAgo = "2 days ago", + className = "", + currentUserId, }) => { - const [isMobile, setIsMobile] = useState(false); - const navigate = useNavigate(); + const [isMobile, setIsMobile] = useState(false); + const navigate = useNavigate(); - const [createConversation, { loading: isCreating }] = useMutation< - CreateConversationMutation, - CreateConversationMutationVariables - >(CreateConversationDocument, { - refetchQueries: [ - { - query: HomeConversationListContainerConversationsByUserDocument, - variables: { userId: currentUserId }, - } - ], - awaitRefetchQueries: true, - onCompleted: (data) => { - if (data.createConversation.status.success) { - navigate('/messages', { - state: { - selectedConversationId: data.createConversation.conversation?.id, - }, - replace: false, - }); - } else { - console.log('Failed to create conversation:', data.createConversation.status.errorMessage); - } - }, - onError: (error) => { - console.error('Error creating conversation:', error); - }, - }); + const [createConversation, { loading: isCreating }] = useMutation< + CreateConversationMutation, + CreateConversationMutationVariables + >(CreateConversationDocument, { + refetchQueries: [ + { + query: HomeConversationListContainerConversationsByUserDocument, + variables: { userId: currentUserId }, + }, + ], + awaitRefetchQueries: true, + onCompleted: (data) => { + if (data.createConversation.status.success) { + navigate("/messages", { + state: { + selectedConversationId: data.createConversation.conversation?.id, + }, + replace: false, + }); + } else { + console.log( + "Failed to create conversation:", + data.createConversation.status.errorMessage + ); + } + }, + onError: (error) => { + console.error("Error creating conversation:", error); + }, + }); - const handleMessageSharer = async () => { - if (!currentUserId) { - return; - } - - try { - await createConversation({ - variables: { - input: { - listingId, - sharerId: sharer.id, - reserverId: currentUserId, - }, - }, - }); - } catch (error) { - console.error('Failed to create conversation:', error); - } - }; + const handleMessageSharer = async () => { + if (!currentUserId) { + return; + } - useEffect(() => { - const checkMobile = () => setIsMobile(window.innerWidth <= 600); - checkMobile(); - window.addEventListener('resize', checkMobile); - return () => window.removeEventListener('resize', checkMobile); - }, []); + try { + await createConversation({ + variables: { + input: { + listingId, + sharerId: sharer.id, + reserverId: currentUserId, + }, + }, + }); + } catch (error) { + console.error("Failed to create conversation:", error); + } + }; - return ( - - - - - -
-

- -

-

- shared {sharedTimeAgo} -

-
- - - {!isOwner && currentUserId && ( - - )} - -
- ); + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth <= 600); + checkMobile(); + window.addEventListener("resize", checkMobile); + return () => window.removeEventListener("resize", checkMobile); + }, []); + + return ( + + + + + +
+

+ +

+

+ shared {sharedTimeAgo} +

+
+ + + {!isOwner && currentUserId && ( + + )} + +
+ ); }; diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx index 5efa8e318..d1257b8f7 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx @@ -1,74 +1,74 @@ -import { useParams, useNavigate } from 'react-router-dom'; -import { ProfileView } from '../../account/profile/components/profile-view.tsx'; -import { useQuery } from '@apollo/client/react'; -import { ComponentQueryLoader } from '@sthrift/ui-components'; +import { useParams, useNavigate } from "react-router-dom"; +import { ProfileView } from "../../account/profile/components/profile-view.tsx"; +import { useQuery } from "@apollo/client/react"; +import { ComponentQueryLoader } from "@sthrift/ui-components"; import { - type ItemListing, - HomeViewUserProfileContainerUserByIdDocument, -} from '../../../../../../generated.tsx'; + type ItemListing, + HomeViewUserProfileContainerUserByIdDocument, +} from "../../../../../generated.tsx"; export const UserProfileViewContainer: React.FC = () => { - const { userId } = useParams<{ userId: string }>(); - const navigate = useNavigate(); + const { userId } = useParams<{ userId: string }>(); + const navigate = useNavigate(); - const { - data: userQueryData, - loading: userLoading, - error: userError, - } = useQuery(HomeViewUserProfileContainerUserByIdDocument, { - variables: { userId: userId ?? '' }, - skip: !userId, - }); + const { + data: userQueryData, + loading: userLoading, + error: userError, + } = useQuery(HomeViewUserProfileContainerUserByIdDocument, { + variables: { userId: userId ?? "" }, + skip: !userId, + }); - const handleListingClick = (listingId: string) => { - navigate(`/listing/${listingId}`); - }; + const handleListingClick = (listingId: string) => { + navigate(`/listing/${listingId}`); + }; - if (!userId) { - return
User ID is required
; - } + if (!userId) { + return
User ID is required
; + } - const viewedUser = userQueryData?.userById; + const viewedUser = userQueryData?.userById; - const buildProfileUser = () => { - if (!viewedUser) return null; - - const { account, createdAt } = viewedUser; - return { - id: viewedUser.id, - firstName: account?.profile?.firstName || '', - lastName: account?.profile?.lastName || '', - username: account?.username || '', - email: '', // Don't expose email for other users - accountType: account?.accountType || '', - location: { - city: account?.profile?.location?.city || '', - state: account?.profile?.location?.state || '', - }, - createdAt: createdAt || '', - }; - }; + const buildProfileUser = () => { + if (!viewedUser) return null; - // For viewing other users' profiles, we don't show their listings - // This would require a backend query to fetch public listings by user ID - const listings: ItemListing[] = []; + const { account, createdAt } = viewedUser; + return { + id: viewedUser.id, + firstName: account?.profile?.firstName || "", + lastName: account?.profile?.lastName || "", + username: account?.username || "", + email: "", // Don't expose email for other users + accountType: account?.accountType || "", + location: { + city: account?.profile?.location?.city || "", + state: account?.profile?.location?.state || "", + }, + createdAt: createdAt || "", + }; + }; - const profileUser = buildProfileUser(); + // For viewing other users' profiles, we don't show their listings + // This would require a backend query to fetch public listings by user ID + const listings: ItemListing[] = []; - return ( - {}} - onListingClick={handleListingClick} - /> - } - /> - ); + const profileUser = buildProfileUser(); + + return ( + {}} + onListingClick={handleListingClick} + /> + } + /> + ); }; diff --git a/apps/ui-sharethrift/src/components/layouts/home/index.tsx b/apps/ui-sharethrift/src/components/layouts/home/index.tsx index 9872060c5..461c0a79d 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/index.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/index.tsx @@ -18,6 +18,7 @@ export const HomeRoutes: React.FC = () => { }> } /> } /> + } /> {showAvatar && !isOwn && ( - + )} {showAvatar && isOwn &&
} {!showAvatar &&
} From 42237120faf7b13a3781a205eaea86c1397baf62 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Mon, 1 Dec 2025 19:49:29 +0530 Subject: [PATCH 07/42] fix: update package.json to include node-forge dependency --- package.json | 133 ++++++++++++++++++++++++++------------------------- 1 file changed, 67 insertions(+), 66 deletions(-) diff --git a/package.json b/package.json index f8d352fa0..7f4acd192 100644 --- a/package.json +++ b/package.json @@ -1,67 +1,68 @@ { - "name": "sharethrift", - "version": "1.0.0", - "description": "", - "type": "module", - "author": "Simnova", - "license": "MIT", - "packageManager": "pnpm@10.18.2", - "main": "apps/api/dist/src/index.js", - "scripts": { - "analyze": "pnpm -r exec -- pnpm dlx @e18e/cli analyze", - "build": "turbo run build", - "test": "turbo run test", - "lint": "turbo run lint", - "dev": "pnpm run build && turbo run azurite gen:watch start --parallel", - "start": "turbo run build && concurrently pnpm:start:* --kill-others-on-fail --workspace=@sthrift/api", - "format": "turbo run format", - "gen": "graphql-codegen --config codegen.yml", - "gen:watch": "graphql-codegen --config codegen.yml --watch", - "tsbuild": "tsc --build", - "tswatch": "tsc --build --watch", - "clean": "pnpm install && turbo run clean && rimraf dist node_modules **/coverage", - "start:api": "pnpm run start --workspace=@sthrift/api", - "start:ui-sharethrift": "pnpm run dev --workspace=@sthrift/ui-sharethrift", - "start-emulator:mongo-memory-server": "pnpm run start --workspace=@cellix/mock-mongodb-memory-server", - "start-emulator:auth-server": "pnpm run start --workspace=@cellix/mock-oauth2-server", - "start-emulator:payment-server": "pnpm run start --workspace=@cellix/mock-payment-server", - "start-emulator:messaging-server": "pnpm run start --workspace=@sthrift/mock-messaging-server", - "test:all": "turbo run test:all", - "test:coverage": "turbo run test:coverage", - "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", - "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", - "test:integration": "turbo run test:integration", - "test:serenity": "turbo run test:serenity", - "test:unit": "turbo run test:unit", - "test:watch": "turbo run test:watch --concurrency 15", - "sonar": "sonar-scanner", - "sonar:pr": "export PR_NUMBER=$(node build-pipeline/scripts/get-pr-number.cjs) && sonar-scanner -Dsonar.pullrequest.key=$PR_NUMBER -Dsonar.pullrequest.branch=$(git branch --show-current) -Dsonar.pullrequest.base=main", - "sonar:pr-windows": "for /f %i in ('node build-pipeline/scripts/get-pr-number.cjs') do set PR_NUMBER=%i && sonar-scanner -Dsonar.pullrequest.key=%PR_NUMBER% -Dsonar.pullrequest.branch=%BRANCH_NAME% -Dsonar.pullrequest.base=main", - "verify": "pnpm run test:coverage:merge && pnpm run sonar:pr && pnpm run check-sonar" - }, - "devDependencies": { - "@amiceli/vitest-cucumber": "^5.1.2", - "@biomejs/biome": "2.0.0", - "@graphql-codegen/cli": "^5.0.7", - "@graphql-codegen/introspection": "^4.0.3", - "@graphql-codegen/typed-document-node": "^5.1.2", - "@graphql-codegen/typescript": "^4.1.6", - "@graphql-codegen/typescript-operations": "^4.6.1", - "@graphql-codegen/typescript-resolvers": "^4.5.1", - "@parcel/watcher": "^2.5.1", - "@playwright/test": "^1.55.1", - "@sonar/scan": "^4.3.0", - "@types/node": "^24.7.2", - "@vitest/coverage-v8": "^3.2.4", - "azurite": "^3.35.0", - "concurrently": "^9.1.2", - "cpx2": "^3.0.2", - "rimraf": "^6.0.1", - "rollup": "3.29.4", - "tsx": "^4.20.3", - "turbo": "^2.5.8", - "typescript": "^5.8.3", - "vite": "^7.0.4", - "vitest": "^3.2.4" - } -} \ No newline at end of file + "name": "sharethrift", + "version": "1.0.0", + "description": "", + "type": "module", + "author": "Simnova", + "license": "MIT", + "packageManager": "pnpm@10.18.2", + "main": "apps/api/dist/src/index.js", + "scripts": { + "analyze": "pnpm -r exec -- pnpm dlx @e18e/cli analyze", + "build": "turbo run build", + "test": "turbo run test", + "lint": "turbo run lint", + "dev": "pnpm run build && turbo run azurite gen:watch start --parallel", + "start": "turbo run build && concurrently pnpm:start:* --kill-others-on-fail --workspace=@sthrift/api", + "format": "turbo run format", + "gen": "graphql-codegen --config codegen.yml", + "gen:watch": "graphql-codegen --config codegen.yml --watch", + "tsbuild": "tsc --build", + "tswatch": "tsc --build --watch", + "clean": "pnpm install && turbo run clean && rimraf dist node_modules **/coverage", + "start:api": "pnpm run start --workspace=@sthrift/api", + "start:ui-sharethrift": "pnpm run dev --workspace=@sthrift/ui-sharethrift", + "start-emulator:mongo-memory-server": "pnpm run start --workspace=@cellix/mock-mongodb-memory-server", + "start-emulator:auth-server": "pnpm run start --workspace=@cellix/mock-oauth2-server", + "start-emulator:payment-server": "pnpm run start --workspace=@cellix/mock-payment-server", + "start-emulator:messaging-server": "pnpm run start --workspace=@sthrift/mock-messaging-server", + "test:all": "turbo run test:all", + "test:coverage": "turbo run test:coverage", + "test:coverage:merge": "pnpm run test:coverage && pnpm run merge-lcov-reports", + "merge-lcov-reports": "node build-pipeline/scripts/merge-coverage.js", + "test:integration": "turbo run test:integration", + "test:serenity": "turbo run test:serenity", + "test:unit": "turbo run test:unit", + "test:watch": "turbo run test:watch --concurrency 15", + "sonar": "sonar-scanner", + "sonar:pr": "export PR_NUMBER=$(node build-pipeline/scripts/get-pr-number.cjs) && sonar-scanner -Dsonar.pullrequest.key=$PR_NUMBER -Dsonar.pullrequest.branch=$(git branch --show-current) -Dsonar.pullrequest.base=main", + "sonar:pr-windows": "for /f %i in ('node build-pipeline/scripts/get-pr-number.cjs') do set PR_NUMBER=%i && sonar-scanner -Dsonar.pullrequest.key=%PR_NUMBER% -Dsonar.pullrequest.branch=%BRANCH_NAME% -Dsonar.pullrequest.base=main", + "verify": "pnpm run test:coverage:merge && pnpm run sonar:pr && pnpm run check-sonar" + }, + "devDependencies": { + "@amiceli/vitest-cucumber": "^5.1.2", + "@biomejs/biome": "2.0.0", + "@graphql-codegen/cli": "^5.0.7", + "@graphql-codegen/introspection": "^4.0.3", + "@graphql-codegen/typed-document-node": "^5.1.2", + "@graphql-codegen/typescript": "^4.1.6", + "@graphql-codegen/typescript-operations": "^4.6.1", + "@graphql-codegen/typescript-resolvers": "^4.5.1", + "@parcel/watcher": "^2.5.1", + "@playwright/test": "^1.55.1", + "@sonar/scan": "^4.3.0", + "@types/node": "^24.7.2", + "@vitest/coverage-v8": "^3.2.4", + "azurite": "^3.35.0", + "concurrently": "^9.1.2", + "cpx2": "^3.0.2", + "rimraf": "^6.0.1", + "rollup": "3.29.4", + "tsx": "^4.20.3", + "turbo": "^2.5.8", + "typescript": "^5.8.3", + "vite": "^7.0.4", + "vitest": "^3.2.4", + "node-forge": "^1.3.2" + } +} From 8a6b7f127e7cc3bd8db71cb6d176cefa962568dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:53:09 +0000 Subject: [PATCH 08/42] fix: address code review feedback - fix layout component, user names in messages, remove unused dependency Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../src/components/layouts/home/index.tsx | 2 +- .../messages/components/conversation-box.tsx | 26 +++++++++++++++++++ .../messages/components/message-thread.tsx | 24 +++++++++++++++-- .../stories/MessageThread.stories.tsx | 22 +++++++++++++++- package.json | 3 +-- 5 files changed, 71 insertions(+), 6 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/index.tsx b/apps/ui-sharethrift/src/components/layouts/home/index.tsx index 3ab480c2d..63c4113af 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/index.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/index.tsx @@ -15,7 +15,7 @@ import { RequireAuthAdmin } from "../../shared/require-auth-admin.tsx"; export const HomeRoutes: React.FC = () => { return ( - }> + }> } /> } /> } /> diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx index 35feb621b..da8c72bbd 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx @@ -7,6 +7,19 @@ interface ConversationBoxProps { data: Conversation; } +// Helper to extract display name from User type +const getUserDisplayName = (user: Conversation['sharer'] | Conversation['reserver'] | undefined): string => { + if (!user) return "Unknown"; + // Handle both PersonalUser and AdminUser account structures + const account = 'account' in user ? user.account : undefined; + const firstName = account?.profile?.firstName; + const lastName = account?.profile?.lastName; + if (firstName || lastName) { + return [firstName, lastName].filter(Boolean).join(" "); + } + return account?.username || "Unknown"; +}; + export const ConversationBox: React.FC = (props) => { const [messageText, setMessageText] = useState(""); @@ -17,6 +30,17 @@ export const ConversationBox: React.FC = (props) => { console.log("Send message logic to be implemented", messageText); }; + // Build user info for sharer and reserver + const sharerInfo = props.data?.sharer ? { + id: props.data.sharer.id, + displayName: getUserDisplayName(props.data.sharer), + } : undefined; + + const reserverInfo = props.data?.reserver ? { + id: props.data.reserver.id, + displayName: getUserDisplayName(props.data.reserver), + } : undefined; + return ( <>
@@ -41,6 +65,8 @@ export const ConversationBox: React.FC = (props) => { setMessageText={setMessageText} handleSendMessage={handleSendMessage} currentUserId={currentUserId} + sharer={sharerInfo} + reserver={reserverInfo} />
diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx index da7a1c1ab..95db4de89 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx @@ -11,6 +11,11 @@ interface Message { createdAt: string; } +interface UserInfo { + id: string; + displayName: string; +} + interface MessageThreadProps { conversationId: string; messages: Message[]; @@ -22,11 +27,24 @@ interface MessageThreadProps { handleSendMessage: (e: React.FormEvent) => void; currentUserId: string; contentContainerStyle?: React.CSSProperties; + sharer?: UserInfo; + reserver?: UserInfo; } export const MessageThread: React.FC = (props) => { const messagesEndRef = useRef(null); + // Helper to get display name for a given authorId + const getAuthorDisplayName = (authorId: string): string => { + if (props.sharer?.id === authorId) { + return props.sharer.displayName || "Sharer"; + } + if (props.reserver?.id === authorId) { + return props.reserver.displayName || "Reserver"; + } + return "User"; + }; + if (props.loading) { return ( = (props) => { (index > 0 && props.messages[index - 1]?.authorId !== message.authorId) } + authorDisplayName={getAuthorDisplayName(message.authorId)} /> )} /> @@ -156,9 +175,10 @@ interface MessageBubbleProps { message: Message; isOwn: boolean; showAvatar: boolean; + authorDisplayName: string; } -function MessageBubble({ message, isOwn, showAvatar }: MessageBubbleProps) { +function MessageBubble({ message, isOwn, showAvatar, authorDisplayName }: MessageBubbleProps) { const formatTime = (dateString: string) => { const date = new Date(dateString); return date.toLocaleTimeString("en-US", { @@ -186,7 +206,7 @@ function MessageBubble({ message, isOwn, showAvatar }: MessageBubbleProps) { }} > {showAvatar && !isOwn && ( - + )} {showAvatar && isOwn &&
} {!showAvatar &&
} diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/MessageThread.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/MessageThread.stories.tsx index 62c494c63..f3a8cc8b9 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/MessageThread.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/MessageThread.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MessageThread } from "../components/message-thread.tsx"; +import { BrowserRouter } from "react-router-dom"; const mockMessages = [ { @@ -14,15 +15,32 @@ const mockMessages = [ id: "m2", messagingMessageId: "SM2", conversationId: "1", - authorId: "Alice", + authorId: "alice456", content: "Yes, it is! Do you want to see it?", createdAt: "2025-08-08T12:02:00Z", }, ]; +const mockSharer = { + id: "alice456", + displayName: "Alice Johnson", +}; + +const mockReserver = { + id: "user123", + displayName: "Bob Smith", +}; + const meta: Meta = { title: "Components/Messages/MessageThread", component: MessageThread, + decorators: [ + (Story) => ( + + + + ), + ], }; export default meta; type Story = StoryObj; @@ -43,5 +61,7 @@ export const Default: Story = { }, currentUserId: "user123", contentContainerStyle: { paddingLeft: 24 }, + sharer: mockSharer, + reserver: mockReserver, }, }; diff --git a/package.json b/package.json index 614f69960..4c33ee1e2 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "turbo": "^2.5.8", "typescript": "^5.8.3", "vite": "^7.0.4", - "vitest": "^3.2.4", - "node-forge": "^1.3.2" + "vitest": "^3.2.4" } } From ebcf82dba63eff0757737d782d71ced17d6835ed Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Fri, 5 Dec 2025 23:33:32 +0530 Subject: [PATCH 09/42] Fix : Build error issue --- .../cellix/mock-payment-server/package.json | 5 ++- pnpm-lock.yaml | 33 ++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/cellix/mock-payment-server/package.json b/packages/cellix/mock-payment-server/package.json index 198b5e777..bc8c4efa1 100644 --- a/packages/cellix/mock-payment-server/package.json +++ b/packages/cellix/mock-payment-server/package.json @@ -6,14 +6,13 @@ "type": "module", "license": "MIT", "dependencies": { + "@cellix/payment-service": "workspace:*", "express": "^4.18.2", "jose": "^5.10.0", - "jsonwebtoken": "^9.0.2", - "@cellix/payment-service": "workspace:*" + "jsonwebtoken": "^9.0.3" }, "devDependencies": { "@cellix/typescript-config": "workspace:*", - "@types/express": "^4.17.21", "@types/jsonwebtoken": "^9.0.10", "ts-node": "^10.9.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f028fdb4..84f00f5d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -516,8 +516,8 @@ importers: specifier: ^5.10.0 version: 5.10.0 jsonwebtoken: - specifier: ^9.0.2 - version: 9.0.2 + specifier: ^9.0.3 + version: 9.0.3 devDependencies: '@cellix/typescript-config': specifier: workspace:* @@ -7783,6 +7783,10 @@ packages: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + jwa@1.4.2: resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} @@ -7795,6 +7799,9 @@ packages: jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + jwt-decode@4.0.0: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} engines: {node: '>=18'} @@ -12237,7 +12244,7 @@ snapshots: '@azure/msal-node@3.8.1': dependencies: '@azure/msal-common': 15.13.1 - jsonwebtoken: 9.0.2 + jsonwebtoken: 9.0.3 uuid: 8.3.2 '@azure/opentelemetry-instrumentation-azure-sdk@1.0.0-beta.9': @@ -20090,6 +20097,19 @@ snapshots: ms: 2.1.3 semver: 7.7.3 + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + jwa@1.4.2: dependencies: buffer-equal-constant-time: 1.0.1 @@ -20112,6 +20132,11 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + jwt-decode@4.0.0: {} jwt-simple@0.5.6: {} @@ -23792,7 +23817,7 @@ snapshots: axios: 1.13.1 dayjs: 1.11.18 https-proxy-agent: 5.0.1 - jsonwebtoken: 9.0.2 + jsonwebtoken: 9.0.3 qs: 6.14.0 scmp: 2.1.0 url-parse: 1.5.10 From ffdd263845ef161d42deeee489207deac80ed4d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:38:26 +0000 Subject: [PATCH 10/42] refactor: address code review feedback - add JSDoc, extract shared utilities, memoize helpers Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../user-profile-view.container.graphql | 2 + .../user-profile-view.container.tsx | 36 ++++++---- .../src/components/layouts/home/index.tsx | 1 + .../messages/components/conversation-box.tsx | 8 +-- .../messages/components/message-thread.tsx | 8 +-- .../home/pages/view-user-profile-page.tsx | 5 ++ .../components/shared/sharethrift-logo.tsx | 43 ++++++++++++ .../src/components/shared/user-avatar.tsx | 40 ++++++----- .../components/shared/user-profile-link.tsx | 21 ++++-- .../shared/utils/user-validation.ts | 12 ++++ .../cellix/mock-oauth2-server/mock-users.json | 70 +++++++++---------- .../cellix/mock-oauth2-server/package.json | 60 ++++++++-------- .../src/setup-environment.ts | 10 +-- packages/cellix/mock-oauth2-server/turbo.json | 6 +- 14 files changed, 206 insertions(+), 116 deletions(-) create mode 100644 apps/ui-sharethrift/src/components/shared/sharethrift-logo.tsx create mode 100644 apps/ui-sharethrift/src/components/shared/utils/user-validation.ts diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.graphql b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.graphql index 32195e207..03f69a942 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.graphql +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.graphql @@ -1,3 +1,5 @@ +# Public query - no authentication required +# Returns limited profile information for any user query HomeViewUserProfileContainerUserById($userId: ObjectID!) { userById(id: $userId) { ... on PersonalUser { diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx index d1257b8f7..5b7af000f 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx @@ -1,4 +1,5 @@ import { useParams, useNavigate } from "react-router-dom"; +import { useMemo, useCallback } from "react"; import { ProfileView } from "../../account/profile/components/profile-view.tsx"; import { useQuery } from "@apollo/client/react"; import { ComponentQueryLoader } from "@sthrift/ui-components"; @@ -7,6 +8,12 @@ import { HomeViewUserProfileContainerUserByIdDocument, } from "../../../../../generated.tsx"; +/** + * Container component for viewing another user's public profile. + * Fetches user data and renders the ProfileView in read-only mode. + * This route is publicly accessible without authentication. + * @returns JSX element containing the user profile view with loading/error states + */ export const UserProfileViewContainer: React.FC = () => { const { userId } = useParams<{ userId: string }>(); const navigate = useNavigate(); @@ -20,19 +27,19 @@ export const UserProfileViewContainer: React.FC = () => { skip: !userId, }); - const handleListingClick = (listingId: string) => { - navigate(`/listing/${listingId}`); - }; - - if (!userId) { - return
User ID is required
; - } - const viewedUser = userQueryData?.userById; - const buildProfileUser = () => { + const handleListingClick = useCallback((listingId: string) => { + navigate(`/listing/${listingId}`); + }, [navigate]); + + // Build profile user data from the query response - memoized for performance + const profileUser = useMemo(() => { if (!viewedUser) return null; + // viewedUser is a union type (PersonalUser | AdminUser) - both have the same account structure + if (!('account' in viewedUser)) return null; + const { account, createdAt } = viewedUser; return { id: viewedUser.id, @@ -47,13 +54,16 @@ export const UserProfileViewContainer: React.FC = () => { }, createdAt: createdAt || "", }; - }; + }, [viewedUser]); - // For viewing other users' profiles, we don't show their listings - // This would require a backend query to fetch public listings by user ID + // TODO: Implement public listings query for user profiles + // Currently, viewing other users' profiles doesn't show their listings. + // This would require a backend query to fetch public listings by user ID. const listings: ItemListing[] = []; - const profileUser = buildProfileUser(); + if (!userId) { + return
User ID is required
; + } return ( { }> } /> } /> + {/* Public route - user profiles are viewable without authentication */} } /> { if (!user) return "Unknown"; // Handle both PersonalUser and AdminUser account structures @@ -25,10 +25,10 @@ export const ConversationBox: React.FC = (props) => { const currentUserId = props?.data?.sharer?.id; - const handleSendMessage = (e: React.FormEvent) => { + const handleSendMessage = useCallback((e: React.FormEvent) => { e.preventDefault(); console.log("Send message logic to be implemented", messageText); - }; + }, [messageText]); // Build user info for sharer and reserver const sharerInfo = props.data?.sharer ? { diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx index 95db4de89..8d076f821 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx @@ -1,6 +1,6 @@ import { List, Input, Button, Spin, Empty, message as antdMessage } from "antd"; import { SendOutlined } from "@ant-design/icons"; -import { useRef } from "react"; +import { useRef, useCallback } from "react"; import { UserAvatar } from "../../../../shared/user-avatar.tsx"; interface Message { @@ -34,8 +34,8 @@ interface MessageThreadProps { export const MessageThread: React.FC = (props) => { const messagesEndRef = useRef(null); - // Helper to get display name for a given authorId - const getAuthorDisplayName = (authorId: string): string => { + // Helper to get display name for a given authorId - memoized for performance + const getAuthorDisplayName = useCallback((authorId: string): string => { if (props.sharer?.id === authorId) { return props.sharer.displayName || "Sharer"; } @@ -43,7 +43,7 @@ export const MessageThread: React.FC = (props) => { return props.reserver.displayName || "Reserver"; } return "User"; - }; + }, [props.sharer, props.reserver]); if (props.loading) { return ( diff --git a/apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.tsx b/apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.tsx index fdfefc109..186e94d26 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.tsx @@ -1,5 +1,10 @@ import { UserProfileViewContainer } from '../components/view-user-profile/user-profile-view.container.tsx'; +/** + * Page component for viewing a user's public profile. + * This is a public route - no authentication required. + * @returns JSX element containing the user profile view + */ export const ViewUserProfile: React.FC = () => { return ; }; diff --git a/apps/ui-sharethrift/src/components/shared/sharethrift-logo.tsx b/apps/ui-sharethrift/src/components/shared/sharethrift-logo.tsx new file mode 100644 index 000000000..f78128cc9 --- /dev/null +++ b/apps/ui-sharethrift/src/components/shared/sharethrift-logo.tsx @@ -0,0 +1,43 @@ +/** + * ShareThrift brand logo component + * Reusable SVG logo for use across the application + */ + +interface ShareThriftLogoProps { + /** Size of the logo in pixels (default: 24) */ + size?: number; + /** Fill color for the logo (default: 'var(--color-foreground-1)') */ + fill?: string; + /** Additional CSS class names */ + className?: string; + /** Custom inline styles */ + style?: React.CSSProperties; +} + +/** + * ShareThrift brand logo SVG component + * @param props - The component props + * @returns JSX element containing the ShareThrift logo + */ +export const ShareThriftLogo: React.FC = ({ + size = 24, + fill = 'var(--color-foreground-1)', + className, + style, +}) => ( + + ShareThrift Logo + + +); diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx index cb5973f0d..96b8e84f8 100644 --- a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx @@ -1,6 +1,18 @@ import { Avatar as AntAvatar } from 'antd'; import { Link } from 'react-router-dom'; +import { isValidUserId } from './utils/user-validation.ts'; +import { ShareThriftLogo } from './sharethrift-logo.tsx'; +/** + * Props for UserAvatar component + * @property userId - The unique identifier for the user (ObjectID) + * @property userName - Display name for accessibility and fallback initial + * @property size - Avatar size in pixels (default: 48) + * @property avatarUrl - Optional URL to user's avatar image + * @property className - Additional CSS classes + * @property style - Inline styles + * @property shape - Avatar shape ('circle' | 'square') + */ export interface UserAvatarProps { userId: string; userName: string; @@ -11,6 +23,13 @@ export interface UserAvatarProps { shape?: 'circle' | 'square'; } +/** + * UserAvatar component displays a user's avatar that optionally links to their profile. + * When a valid userId is provided, clicking the avatar navigates to the user's profile. + * Falls back to a non-clickable avatar when userId is missing or invalid. + * @param props - The component props + * @returns JSX element containing the avatar, optionally wrapped in a link + */ export const UserAvatar: React.FC = ({ userId, userName, @@ -20,8 +39,8 @@ export const UserAvatar: React.FC = ({ style = {}, shape = 'circle', }) => { - // Determine if we have a valid userId for linking (non-empty and non-whitespace) - const isClickable = !!userId && userId.trim() !== ''; + // Determine if we have a valid userId for linking + const isClickable = isValidUserId(userId); const avatarContent = avatarUrl ? ( = ({ cursor: isClickable ? 'pointer' : 'default', ...style, }} - icon={ - - Sharethrift Logo - - - } + icon={} > {userName.charAt(0).toUpperCase()} diff --git a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx index 0d0e5c9de..0f3eb6736 100644 --- a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx @@ -1,5 +1,13 @@ import { Link } from 'react-router-dom'; +import { isValidUserId } from './utils/user-validation.ts'; +/** + * Props for UserProfileLink component + * @property userId - The unique identifier for the user (ObjectID) + * @property displayName - The text to display as the clickable link + * @property className - Additional CSS classes + * @property style - Custom inline styles + */ export interface UserProfileLinkProps { userId: string; displayName: string; @@ -7,16 +15,21 @@ export interface UserProfileLinkProps { style?: React.CSSProperties; } +/** + * UserProfileLink component renders a clickable link to a user's profile. + * When a valid userId is provided, renders a navigation link. + * Falls back to plain text when userId is missing or invalid. + * @param props - The component props + * @returns JSX element containing either a link or plain text + */ export const UserProfileLink: React.FC = ({ userId, displayName, className = '', style = {}, }) => { - // If no valid userId (empty or whitespace-only), render as plain text instead of a broken link - const isValidUserId = !!userId && userId.trim() !== ''; - - if (!isValidUserId) { + // If no valid userId, render as plain text instead of a broken link + if (!isValidUserId(userId)) { return ( {displayName} diff --git a/apps/ui-sharethrift/src/components/shared/utils/user-validation.ts b/apps/ui-sharethrift/src/components/shared/utils/user-validation.ts new file mode 100644 index 000000000..8d1f81b79 --- /dev/null +++ b/apps/ui-sharethrift/src/components/shared/utils/user-validation.ts @@ -0,0 +1,12 @@ +/** + * Utility functions for user-related validations + */ + +/** + * Validates if a userId is valid (non-empty and non-whitespace) + * @param userId - The user ID to validate + * @returns true if the userId is valid, false otherwise + */ +export const isValidUserId = (userId: string | undefined | null): boolean => { + return !!userId && userId.trim() !== ''; +}; diff --git a/packages/cellix/mock-oauth2-server/mock-users.json b/packages/cellix/mock-oauth2-server/mock-users.json index 63fdf1617..1bd05d953 100644 --- a/packages/cellix/mock-oauth2-server/mock-users.json +++ b/packages/cellix/mock-oauth2-server/mock-users.json @@ -1,36 +1,36 @@ { - "users": { - "915d7603-a146-4aef-b5ec-12861642eddd": { - "aud": "test-client-id", - "sub": "915d7603-a146-4aef-b5ec-12861642eddd", - "iss": "http://localhost:4001", - "email": "nkduy2011@gmail.com", - "given_name": "Duy", - "family_name": "Nguyen", - "tid": "test-tenant-id" - }, - "a53e5635-d0d1-4c15-92b2-ebb298c84772": { - "aud": "test-client-id", - "sub": "a53e5635-d0d1-4c15-92b2-ebb298c84772", - "iss": "http://localhost:4001", - "email": "nkduy2011@gmail.com", - "given_name": "Duy", - "family_name": "Nguyen", - "tid": "test-tenant-id" - }, - "9ddf640b-9676-44af-97b4-152883c03fdf": { - "aud": "test-client-id", - "sub": "9ddf640b-9676-44af-97b4-152883c03fdf", - "iss": "http://localhost:4001", - "email": "nkduy2011@gmail.com", - "given_name": "Duy", - "family_name": "Nguyen", - "tid": "test-tenant-id" - } - }, - "refreshTokens": { - "0d46da63-0b08-4e6b-a247-9c6a13f799d6": "915d7603-a146-4aef-b5ec-12861642eddd", - "aed57ef7-dfdd-40f5-8a0a-dfe3f79f34d6": "a53e5635-d0d1-4c15-92b2-ebb298c84772", - "4b397cc7-0367-43dc-ad23-08b450ff0454": "9ddf640b-9676-44af-97b4-152883c03fdf" - } -} \ No newline at end of file + "users": { + "915d7603-a146-4aef-b5ec-12861642eddd": { + "aud": "test-client-id", + "sub": "915d7603-a146-4aef-b5ec-12861642eddd", + "iss": "http://localhost:4001", + "email": "nkduy2011@gmail.com", + "given_name": "Duy", + "family_name": "Nguyen", + "tid": "test-tenant-id" + }, + "a53e5635-d0d1-4c15-92b2-ebb298c84772": { + "aud": "test-client-id", + "sub": "a53e5635-d0d1-4c15-92b2-ebb298c84772", + "iss": "http://localhost:4001", + "email": "nkduy2011@gmail.com", + "given_name": "Duy", + "family_name": "Nguyen", + "tid": "test-tenant-id" + }, + "9ddf640b-9676-44af-97b4-152883c03fdf": { + "aud": "test-client-id", + "sub": "9ddf640b-9676-44af-97b4-152883c03fdf", + "iss": "http://localhost:4001", + "email": "nkduy2011@gmail.com", + "given_name": "Duy", + "family_name": "Nguyen", + "tid": "test-tenant-id" + } + }, + "refreshTokens": { + "0d46da63-0b08-4e6b-a247-9c6a13f799d6": "915d7603-a146-4aef-b5ec-12861642eddd", + "aed57ef7-dfdd-40f5-8a0a-dfe3f79f34d6": "a53e5635-d0d1-4c15-92b2-ebb298c84772", + "4b397cc7-0367-43dc-ad23-08b450ff0454": "9ddf640b-9676-44af-97b4-152883c03fdf" + } +} diff --git a/packages/cellix/mock-oauth2-server/package.json b/packages/cellix/mock-oauth2-server/package.json index 1f402e89d..e95a82efe 100644 --- a/packages/cellix/mock-oauth2-server/package.json +++ b/packages/cellix/mock-oauth2-server/package.json @@ -1,31 +1,31 @@ { - "name": "@cellix/mock-oauth2-server", - "version": "1.0.0", - "author": "", - "license": "MIT", - "description": "Local OAuth2/OIDC mock server for dev/testing", - "type": "module", - "main": "dist/src/index.js", - "types": "dist/src/index.d.ts", - "scripts": { - "build": "tsc --build", - "clean": "rimraf dist node_modules tsconfig.tsbuildinfo && tsc --build --clean", - "lint": "biome lint", - "format": "biome format --write", - "start": "node dist/src/index.js", - "dev": "tsx watch src/index.ts", - "prebuild": "biome lint" - }, - "dependencies": { - "dotenv": "^16.4.5", - "express": "^4.22.1", - "jose": "^5.9.6" - }, - "devDependencies": { - "@cellix/typescript-config": "workspace:*", - "@types/express": "^4.17.23", - "rimraf": "^6.0.1", - "tsx": "^4.20.3", - "typescript": "^5.8.3" - } -} \ No newline at end of file + "name": "@cellix/mock-oauth2-server", + "version": "1.0.0", + "author": "", + "license": "MIT", + "description": "Local OAuth2/OIDC mock server for dev/testing", + "type": "module", + "main": "dist/src/index.js", + "types": "dist/src/index.d.ts", + "scripts": { + "build": "tsc --build", + "clean": "rimraf dist node_modules tsconfig.tsbuildinfo && tsc --build --clean", + "lint": "biome lint", + "format": "biome format --write", + "start": "node dist/src/index.js", + "dev": "tsx watch src/index.ts", + "prebuild": "biome lint" + }, + "dependencies": { + "dotenv": "^16.4.5", + "express": "^4.22.1", + "jose": "^5.9.6" + }, + "devDependencies": { + "@cellix/typescript-config": "workspace:*", + "@types/express": "^4.17.23", + "rimraf": "^6.0.1", + "tsx": "^4.20.3", + "typescript": "^5.8.3" + } +} diff --git a/packages/cellix/mock-oauth2-server/src/setup-environment.ts b/packages/cellix/mock-oauth2-server/src/setup-environment.ts index 5ace1559d..9fa5346bf 100644 --- a/packages/cellix/mock-oauth2-server/src/setup-environment.ts +++ b/packages/cellix/mock-oauth2-server/src/setup-environment.ts @@ -1,8 +1,8 @@ import dotenv from 'dotenv'; export const setupEnvironment = () => { - console.log('Setting up environment variables'); - dotenv.config(); - dotenv.config({ path: `.env.local`, override: true }); - console.log('Environment variables set up'); -} \ No newline at end of file + console.log('Setting up environment variables'); + dotenv.config(); + dotenv.config({ path: `.env.local`, override: true }); + console.log('Environment variables set up'); +}; diff --git a/packages/cellix/mock-oauth2-server/turbo.json b/packages/cellix/mock-oauth2-server/turbo.json index a10129ea4..6403b5e05 100644 --- a/packages/cellix/mock-oauth2-server/turbo.json +++ b/packages/cellix/mock-oauth2-server/turbo.json @@ -1,4 +1,4 @@ { - "extends": ["//"], - "tags": ["backend"] -} \ No newline at end of file + "extends": ["//"], + "tags": ["backend"] +} From 427e6f8ebb56df717bd3e317a886e0a36e505425 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Wed, 10 Dec 2025 23:49:57 +0530 Subject: [PATCH 11/42] feat: add MemoryRouter decorator to ListingBanner story --- .../home/messages/stories/ListingBanner.stories.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx index 8b284ac32..13f7f4bb6 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx @@ -1,3 +1,4 @@ +import { MemoryRouter } from "react-router-dom"; import type { Meta, StoryObj } from "@storybook/react"; import { ListingBanner, @@ -26,6 +27,13 @@ const mockUser: PersonalUser = { const meta: Meta = { title: "Components/Listings/ListingBanner", component: ListingBanner, + decorators: [ + (Story) => ( + + + + ), + ], }; export default meta; type Story = StoryObj; From ca60e458ada6bea97b69ed8628f45a85c5f89cd7 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Thu, 11 Dec 2025 00:43:49 +0530 Subject: [PATCH 12/42] fix: reorder import statements in ListingBanner story --- .../layouts/home/messages/stories/ListingBanner.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx index 13f7f4bb6..1d99a75af 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx @@ -1,5 +1,5 @@ -import { MemoryRouter } from "react-router-dom"; import type { Meta, StoryObj } from "@storybook/react"; +import { MemoryRouter } from "react-router-dom"; import { ListingBanner, type ListingBannerProps, From cc89d8dc37a360586e552d14f86b933026b33d1c Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Thu, 11 Dec 2025 01:00:46 +0530 Subject: [PATCH 13/42] test: Add comprehensive test coverage for UI components - Add tests to ListingBanner story with MemoryRouter decorator - Add comprehensive tests to UserProfileLink story (4 test scenarios) - Create user-validation utility tests (3 test scenarios) - Fix ListingBanner story router context issue - Increase overall coverage to 87.06% (above 80% threshold) - All 406 tests passing --- .../stories/ListingBanner.stories.tsx | 50 ++++++++++++- .../shared/user-profile-link.stories.tsx | 40 ++++++++++ .../shared/utils/user-validation.stories.tsx | 73 +++++++++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 apps/ui-sharethrift/src/components/shared/utils/user-validation.stories.tsx diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx index 1d99a75af..df2ca8bb8 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx @@ -1,10 +1,12 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MemoryRouter } from "react-router-dom"; +import { expect, within } from "storybook/test"; import { ListingBanner, type ListingBannerProps, } from "../components/listing-banner.tsx"; import type { PersonalUser } from "../../../../../generated.tsx"; + // Mock PersonalUser object for Storybook const mockUser: PersonalUser = { id: "507f1f77bcf86cd799439011", @@ -24,6 +26,20 @@ const mockUser: PersonalUser = { isBlocked: false, }; +const mockUserWithoutProfile: PersonalUser = { + id: "507f1f77bcf86cd799439012", + __typename: "PersonalUser", + account: { + __typename: "PersonalUserAccount", + email: "unknown@example.com", + username: "unknown_user", + profile: undefined, + }, + userType: "personal-user", + hasCompletedOnboarding: false, + isBlocked: false, +} as unknown as PersonalUser; + const meta: Meta = { title: "Components/Listings/ListingBanner", component: ListingBanner, @@ -41,4 +57,36 @@ type Story = StoryObj; export const Default: Story = { args: { owner: mockUser, - } satisfies ListingBannerProps}; + } satisfies ListingBannerProps, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Test owner name display + await expect(canvas.getByText("Alice's Listing")).toBeInTheDocument(); + await expect(canvas.getByText("Alice")).toBeInTheDocument(); + + // Test request period + await expect(canvas.getByText("Request Period:")).toBeInTheDocument(); + await expect(canvas.getByText("1 Month")).toBeInTheDocument(); + + // Test status + await expect(canvas.getByText("Request Submitted")).toBeInTheDocument(); + + // Test profile link + const profileLink = canvas.getByRole("link", { name: "Alice" }); + await expect(profileLink).toBeInTheDocument(); + await expect(profileLink).toHaveAttribute("href", "/user/507f1f77bcf86cd799439011"); + }, +}; + +export const UnknownOwner: Story = { + args: { + owner: mockUserWithoutProfile, + } satisfies ListingBannerProps, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Test fallback for missing profile + await expect(canvas.getByText("Unknown's Listing")).toBeInTheDocument(); + }, +}; diff --git a/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx b/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx index 137cf7b4d..c5f37051c 100644 --- a/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-profile-link.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { BrowserRouter } from 'react-router-dom'; +import { expect, within } from 'storybook/test'; import { UserProfileLink } from './user-profile-link.tsx'; const meta: Meta = { @@ -41,6 +42,14 @@ export const Default: Story = { userId: '507f1f77bcf86cd799439011', displayName: 'John Doe', }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Test that link is rendered + const link = canvas.getByRole('link', { name: 'John Doe' }); + await expect(link).toBeInTheDocument(); + await expect(link).toHaveAttribute('href', '/user/507f1f77bcf86cd799439011'); + }, }; export const WithCustomStyle: Story = { @@ -53,6 +62,17 @@ export const WithCustomStyle: Story = { color: '#ff0000', }, }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Test link is rendered and accessible + const link = canvas.getByRole('link', { name: 'Jane Smith' }); + await expect(link).toBeInTheDocument(); + await expect(link).toHaveAttribute('href', '/user/507f1f77bcf86cd799439011'); + + // Verify custom color is applied + await expect(link).toHaveStyle({ color: 'rgb(255, 0, 0)' }); + }, }; export const WithoutUserId: Story = { @@ -67,6 +87,13 @@ export const WithoutUserId: Story = { }, }, }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Test that plain text is rendered (not a link) + await expect(canvas.getByText('Unknown User')).toBeInTheDocument(); + await expect(canvas.queryByRole('link')).not.toBeInTheDocument(); + }, }; export const InContext: Story = { @@ -80,4 +107,17 @@ export const InContext: Story = {

), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // Test both links are rendered + const aliceLink = canvas.getByRole('link', { name: 'Alice Johnson' }); + const bobLink = canvas.getByRole('link', { name: 'Bob Williams' }); + + await expect(aliceLink).toBeInTheDocument(); + await expect(aliceLink).toHaveAttribute('href', '/user/507f1f77bcf86cd799439011'); + + await expect(bobLink).toBeInTheDocument(); + await expect(bobLink).toHaveAttribute('href', '/user/507f1f77bcf86cd799439012'); + }, }; diff --git a/apps/ui-sharethrift/src/components/shared/utils/user-validation.stories.tsx b/apps/ui-sharethrift/src/components/shared/utils/user-validation.stories.tsx new file mode 100644 index 000000000..2e15dd4e3 --- /dev/null +++ b/apps/ui-sharethrift/src/components/shared/utils/user-validation.stories.tsx @@ -0,0 +1,73 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect } from 'storybook/test'; +import { isValidUserId } from './user-validation.ts'; + +const meta: Meta = { + title: 'Shared/Utils/UserValidation', + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +const ValidationDemo: React.FC<{ userId: string | undefined | null; label: string }> = ({ userId, label }) => { + const isValid = isValidUserId(userId); + return ( +
+ {label}: {JSON.stringify(userId)} +
+ Result: {isValid ? 'Valid' : 'Invalid'} +
+ ); +}; + +export const ValidUserIds: Story = { + render: () => ( +
+

Valid User IDs

+ + + +
+ ), + play: async () => { + // Test valid user IDs + await expect(isValidUserId('507f1f77bcf86cd799439011')).toBe(true); + await expect(isValidUserId('abc123def456')).toBe(true); + await expect(isValidUserId('user-12345')).toBe(true); + }, +}; + +export const InvalidUserIds: Story = { + render: () => ( +
+

Invalid User IDs

+ + + + +
+ ), + play: async () => { + // Test invalid user IDs + await expect(isValidUserId('')).toBe(false); + await expect(isValidUserId(' ')).toBe(false); + await expect(isValidUserId(undefined)).toBe(false); + await expect(isValidUserId(null)).toBe(false); + }, +}; + +export const EdgeCases: Story = { + render: () => ( +
+

Edge Cases

+ + +
+ ), + play: async () => { + // Test edge cases - these should be valid because they have content after trim + await expect(isValidUserId(' validId ')).toBe(true); + await expect(isValidUserId(' tabbed ')).toBe(true); + }, +}; From e5fbc1f093b6c89f9f775a5a9718c0f60667c12a Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Fri, 12 Dec 2025 23:08:58 +0530 Subject: [PATCH 14/42] test: cover reservation request pagination logic --- .../reservation-request.paginate.test.ts | 108 ++++++++++++++++++ .../reservation-request.resolvers.ts | 3 +- 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts new file mode 100644 index 000000000..db7916507 --- /dev/null +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect } from 'vitest'; +import { paginateAndFilterListingRequests } from './reservation-request.resolvers.ts'; + +const baseRequest = { + id: 'req-1', + createdAt: new Date('2025-01-01T00:00:00Z'), + reservationPeriodStart: new Date('2025-01-05T00:00:00Z'), + reservationPeriodEnd: new Date('2025-01-10T00:00:00Z'), + listing: { title: 'Cordless Drill' }, + reserver: { id: 'user-1', account: { username: 'alice' } }, +} as const; + +describe('paginateAndFilterListingRequests', () => { + it('maps domain shape to UI shape with defaults', () => { + const result = paginateAndFilterListingRequests([baseRequest], { + page: 1, + pageSize: 10, + statusFilters: [], + }); + + expect(result.items).toHaveLength(1); + const [item] = result.items; + expect(item.title).toBe('Cordless Drill'); + expect(item.image).toBe('/assets/item-images/placeholder.png'); + expect(item.requestedBy).toBe('@alice'); + expect(item.requestedOn).toBe('2025-01-01T00:00:00.000Z'); + expect(item.reservationPeriod).toBe('2025-01-05 - 2025-01-10'); + expect(item.status).toBe('Pending'); + expect(item._raw.id).toBe('req-1'); + }); + + it('filters by searchText (case-insensitive)', () => { + const result = paginateAndFilterListingRequests( + [ + { ...baseRequest, id: 'a', listing: { title: 'Camera' } }, + { ...baseRequest, id: 'b', listing: { title: 'Drone' } }, + ], + { page: 1, pageSize: 10, statusFilters: [], searchText: 'camera' }, + ); + + expect(result.items.map((i) => i.id)).toEqual(['a']); + }); + + it('filters by statusFilters', () => { + const result = paginateAndFilterListingRequests( + [ + { ...baseRequest, id: 'a', state: 'Pending' }, + { ...baseRequest, id: 'b', state: 'Accepted' }, + ], + { page: 1, pageSize: 10, statusFilters: ['Accepted'] }, + ); + + expect(result.items.map((i) => i.id)).toEqual(['b']); + }); + + it('supports sorting with nulls and mixed values', () => { + const resultAsc = paginateAndFilterListingRequests( + [ + { ...baseRequest, id: 'a', reserver: { id: undefined, account: { username: 'alice' } } }, + { ...baseRequest, id: 'b', reserver: { id: 'user-2', account: { username: 'bob' } } }, + { ...baseRequest, id: 'c', reserver: { id: 'user-1', account: { username: 'cara' } } }, + ], + { + page: 1, + pageSize: 10, + statusFilters: [], + sorter: { field: 'requestedById', order: 'ascend' }, + }, + ); + + expect(resultAsc.items.map((i) => i.id)).toEqual(['a', 'c', 'b']); + + const resultDesc = paginateAndFilterListingRequests( + [ + { ...baseRequest, id: 'a', reserver: { id: undefined, account: { username: 'alice' } } }, + { ...baseRequest, id: 'b', reserver: { id: 'user-2', account: { username: 'bob' } } }, + { ...baseRequest, id: 'c', reserver: { id: 'user-1', account: { username: 'cara' } } }, + ], + { + page: 1, + pageSize: 10, + statusFilters: [], + sorter: { field: 'requestedById', order: 'descend' }, + }, + ); + + expect(resultDesc.items.map((i) => i.id)).toEqual(['b', 'c', 'a']); + }); + + it('paginates results correctly', () => { + const requests = Array.from({ length: 25 }).map((_, index) => ({ + ...baseRequest, + id: `req-${index + 1}`, + })); + + const page2 = paginateAndFilterListingRequests(requests, { + page: 2, + pageSize: 10, + statusFilters: [], + }); + + expect(page2.items).toHaveLength(10); + expect(page2.items[0]?.id).toBe('req-11'); + expect(page2.total).toBe(25); + expect(page2.page).toBe(2); + expect(page2.pageSize).toBe(10); + }); +}); diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts index 989421140..2106c99ae 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts @@ -30,7 +30,8 @@ interface ListingRequestUiShape { [k: string]: unknown; // enable dynamic field sorting access } -function paginateAndFilterListingRequests( +// Exported for unit testing the request mapping/pagination logic +export function paginateAndFilterListingRequests( requests: ListingRequestDomainShape[], options: { page: number; From 0a903d97a06ff24dc6caa97ab30d4757ca741f63 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Mon, 15 Dec 2025 23:41:52 +0530 Subject: [PATCH 15/42] fix: update type assertions in ListingBanner stories and improve error handling in pagination tests --- .../home/messages/stories/ListingBanner.stories.tsx | 6 ++++-- packages/sthrift/graphql/src/helpers/tracing.test.ts | 11 ++++++++--- .../reservation-request.paginate.test.ts | 9 ++++++--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx index 62cb77deb..96af53ff3 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx @@ -5,6 +5,8 @@ import type { ComponentProps } from "react"; import { ListingBanner } from "../components/listing-banner.tsx"; import type { PersonalUser } from "../../../../../generated.tsx"; +type ListingBannerStoryProps = ComponentProps; + // Mock PersonalUser object for Storybook const mockUser: PersonalUser = { id: "507f1f77bcf86cd799439011", @@ -55,7 +57,7 @@ type Story = StoryObj; export const Default: Story = { args: { owner: mockUser, - } satisfies ListingBannerProps, + } satisfies ListingBannerStoryProps, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -80,7 +82,7 @@ export const Default: Story = { export const UnknownOwner: Story = { args: { owner: mockUserWithoutProfile, - } satisfies ListingBannerProps, + } satisfies ListingBannerStoryProps, play: async ({ canvasElement }) => { const canvas = within(canvasElement); diff --git a/packages/sthrift/graphql/src/helpers/tracing.test.ts b/packages/sthrift/graphql/src/helpers/tracing.test.ts index f063e022b..2e191b6ae 100644 --- a/packages/sthrift/graphql/src/helpers/tracing.test.ts +++ b/packages/sthrift/graphql/src/helpers/tracing.test.ts @@ -46,10 +46,15 @@ vi.mock('@opentelemetry/api', async (importOriginal) => { fn: (span: Span) => Promise, ): Promise => { const mockSpan = createMockSpan(); + const globals = global as unknown as { + __mockSpan?: Span; + __mockTracerName?: string; + __mockSpanName?: string; + }; // Store references for assertions - (global as Record)['__mockSpan'] = mockSpan; - (global as Record)['__mockTracerName'] = tracerName; - (global as Record)['__mockSpanName'] = spanName; + globals.__mockSpan = mockSpan; + globals.__mockTracerName = tracerName; + globals.__mockSpanName = spanName; return fn(mockSpan); }, ), diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts index db7916507..88f70e4de 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts @@ -19,7 +19,10 @@ describe('paginateAndFilterListingRequests', () => { }); expect(result.items).toHaveLength(1); - const [item] = result.items; + const item = result.items[0]; + if (!item) { + throw new Error('Expected a single item in results'); + } expect(item.title).toBe('Cordless Drill'); expect(item.image).toBe('/assets/item-images/placeholder.png'); expect(item.requestedBy).toBe('@alice'); @@ -56,7 +59,7 @@ describe('paginateAndFilterListingRequests', () => { it('supports sorting with nulls and mixed values', () => { const resultAsc = paginateAndFilterListingRequests( [ - { ...baseRequest, id: 'a', reserver: { id: undefined, account: { username: 'alice' } } }, + { ...baseRequest, id: 'a', reserver: { account: { username: 'alice' } } }, { ...baseRequest, id: 'b', reserver: { id: 'user-2', account: { username: 'bob' } } }, { ...baseRequest, id: 'c', reserver: { id: 'user-1', account: { username: 'cara' } } }, ], @@ -72,7 +75,7 @@ describe('paginateAndFilterListingRequests', () => { const resultDesc = paginateAndFilterListingRequests( [ - { ...baseRequest, id: 'a', reserver: { id: undefined, account: { username: 'alice' } } }, + { ...baseRequest, id: 'a', reserver: { account: { username: 'alice' } } }, { ...baseRequest, id: 'b', reserver: { id: 'user-2', account: { username: 'bob' } } }, { ...baseRequest, id: 'c', reserver: { id: 'user-1', account: { username: 'cara' } } }, ], From 15b61d81d08919ad24f7add03967dd5aa9f2db55 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 16 Dec 2025 00:25:05 +0530 Subject: [PATCH 16/42] refactor: simplify type exports in sharer information, user avatar, and user profile link components --- .../view-listing/sharer-information/sharer-information.tsx | 4 ++-- apps/ui-sharethrift/src/components/shared/user-avatar.tsx | 2 +- .../src/components/shared/user-profile-link.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx index 0b4345ae7..cd80d1aad 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx @@ -14,13 +14,13 @@ import type { import { UserAvatar } from "../../../../../shared/user-avatar.tsx"; import { UserProfileLink } from "../../../../../shared/user-profile-link.tsx"; -export type Sharer = { +type Sharer = { id: string; name: string; avatar?: string; }; -export interface SharerInformationProps { +interface SharerInformationProps { sharer: Sharer; listingId: string; isOwner?: boolean; diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx index 96b8e84f8..d06aca065 100644 --- a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx @@ -13,7 +13,7 @@ import { ShareThriftLogo } from './sharethrift-logo.tsx'; * @property style - Inline styles * @property shape - Avatar shape ('circle' | 'square') */ -export interface UserAvatarProps { +interface UserAvatarProps { userId: string; userName: string; size?: number; diff --git a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx index 0f3eb6736..79b8f08e3 100644 --- a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx @@ -8,7 +8,7 @@ import { isValidUserId } from './utils/user-validation.ts'; * @property className - Additional CSS classes * @property style - Custom inline styles */ -export interface UserProfileLinkProps { +interface UserProfileLinkProps { userId: string; displayName: string; className?: string; From 5bbd98f807010763744088d34f2e7203acc31f7f Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 16 Dec 2025 22:33:37 +0530 Subject: [PATCH 17/42] feat: add user profile navigation and storybook examples for user profile components --- .../user-profile-view.container.stories.tsx | 82 +++++++++++++++++++ .../components/requests-table.stories.tsx | 70 ++++++++++++++++ .../pages/view-user-profile-page.stories.tsx | 67 +++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.stories.tsx create mode 100644 apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx create mode 100644 apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.stories.tsx diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.stories.tsx new file mode 100644 index 000000000..b3c338f7c --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.stories.tsx @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { MockedProvider } from '@apollo/client/testing/react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { expect, within } from 'storybook/test'; +import { HomeViewUserProfileContainerUserByIdDocument } from '../../../../../generated.tsx'; +import { UserProfileViewContainer } from './user-profile-view.container.tsx'; + +const meta: Meta = { + title: 'Layouts/Home/ViewUserProfile/UserProfileViewContainer', + component: UserProfileViewContainer, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const MissingUserId: Story = { + render: () => ( + + + + + + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByText('User ID is required')).toBeInTheDocument(); + }, +}; + +export const WithProfileData: Story = { + render: () => ( + + + + } /> + + + + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(await canvas.findByText('@alice_doe')).toBeInTheDocument(); + await expect(await canvas.findByText(/Seattle/i)).toBeInTheDocument(); + }, +}; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx new file mode 100644 index 000000000..a0863ea5e --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { MemoryRouter } from 'react-router-dom'; +import { expect, within } from 'storybook/test'; +import { RequestsTable } from './requests-table.tsx'; +import type { ListingRequestData } from './my-listings-dashboard.types.ts'; + +const meta: Meta = { + title: 'Layouts/Home/MyListings/RequestsTable', + component: RequestsTable, + tags: ['autodocs'], + decorators: [ + (Story) => ( + + + + ), + ], +}; + +export default meta; + +type Story = StoryObj; + +const sampleData: ListingRequestData[] = [ + { + id: 'req-1', + title: 'Cordless Drill', + image: '/assets/item-images/placeholder.png', + requestedBy: '@alice', + requestedById: '507f1f77bcf86cd799439011', + requestedOn: '2025-01-01T00:00:00.000Z', + reservationPeriod: '2025-01-05 - 2025-01-10', + status: 'Pending', + }, + { + id: 'req-2', + title: 'Camera', + image: '/assets/item-images/placeholder.png', + requestedBy: '@unknown', + requestedById: null, + requestedOn: '2025-01-02T00:00:00.000Z', + reservationPeriod: 'N/A', + status: 'Accepted', + }, +]; + +export const Default: Story = { + args: { + data: sampleData, + searchText: '', + statusFilters: [], + sorter: { field: null, order: null }, + currentPage: 1, + pageSize: 10, + total: sampleData.length, + loading: false, + onSearch: () => undefined, + onStatusFilter: () => undefined, + onTableChange: () => undefined, + onPageChange: () => undefined, + onAction: () => undefined, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getAllByText('Cordless Drill').length).toBeGreaterThan(0); + const link = canvas.getByRole('link', { name: '@alice' }); + await expect(link).toHaveAttribute('href', '/user/507f1f77bcf86cd799439011'); + await expect(canvas.getAllByText('Accepted').length).toBeGreaterThan(0); + }, +}; diff --git a/apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.stories.tsx new file mode 100644 index 000000000..533d4a3d7 --- /dev/null +++ b/apps/ui-sharethrift/src/components/layouts/home/pages/view-user-profile-page.stories.tsx @@ -0,0 +1,67 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { MockedProvider } from '@apollo/client/testing/react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { expect, within } from 'storybook/test'; +import { HomeViewUserProfileContainerUserByIdDocument } from '../../../../generated.tsx'; +import { ViewUserProfile } from './view-user-profile-page.tsx'; + +const meta: Meta = { + title: 'Layouts/Home/Pages/ViewUserProfile', + component: ViewUserProfile, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + + + } /> + + + + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(await canvas.findByText('@alice_doe')).toBeInTheDocument(); + }, +}; From 6fa5d356f1126936fd5cbccd3bcca033edd4e26c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:30:31 +0000 Subject: [PATCH 18/42] fix: address sourcery-ai feedback - clarify validation function, fix comments, restore link color Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../layouts/home/messages/components/conversation-box.tsx | 3 ++- .../home/my-listings/components/requests-table.tsx | 2 +- .../src/components/shared/utils/user-validation.ts | 8 +++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx index 7fd19ffe7..d9d4c615c 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx @@ -7,7 +7,8 @@ interface ConversationBoxProps { data: Conversation; } -// Helper to extract display name from User type - memoized for performance +// Helper function to extract display name from User type +// Note: This is a module-level pure function. Memoization happens inside the component via useMemo. const getUserDisplayName = (user: Conversation['sharer'] | Conversation['reserver'] | undefined): string => { if (!user) return "Unknown"; // Handle both PersonalUser and AdminUser account structures diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx index 8ecd4826f..b79f24846 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx @@ -114,7 +114,7 @@ export const RequestsTable: React.FC = ({ ); } diff --git a/apps/ui-sharethrift/src/components/shared/utils/user-validation.ts b/apps/ui-sharethrift/src/components/shared/utils/user-validation.ts index 8d1f81b79..89c763ae7 100644 --- a/apps/ui-sharethrift/src/components/shared/utils/user-validation.ts +++ b/apps/ui-sharethrift/src/components/shared/utils/user-validation.ts @@ -3,9 +3,11 @@ */ /** - * Validates if a userId is valid (non-empty and non-whitespace) - * @param userId - The user ID to validate - * @returns true if the userId is valid, false otherwise + * Checks if a userId string is present and non-empty. + * This is a basic check for routing purposes - it does not validate the format or existence + * of the user in the database. Invalid IDs are handled gracefully by the GraphQL layer. + * @param userId - The user ID to check + * @returns true if the userId is a non-empty string, false otherwise */ export const isValidUserId = (userId: string | undefined | null): boolean => { return !!userId && userId.trim() !== ''; From 3930eed85aae319d3659aa4aabdbc590ff307f83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:08:00 +0000 Subject: [PATCH 19/42] fix: address final sourcery-ai feedback - clarify comment and add userName safeguard Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../messages/components/conversation-box.tsx | 129 ++++++++++-------- .../src/components/shared/user-avatar.tsx | 2 +- 2 files changed, 70 insertions(+), 61 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx index d9d4c615c..87bf0ae1a 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx @@ -1,75 +1,84 @@ -import type { Conversation } from "../../../../../generated.tsx"; -import { ListingBanner } from "./listing-banner.tsx"; -import { MessageThread } from "./index.ts"; -import { useState, useCallback } from "react"; +import type { Conversation } from '../../../../../generated.tsx'; +import { ListingBanner } from './listing-banner.tsx'; +import { MessageThread } from './index.ts'; +import { useState, useCallback } from 'react'; interface ConversationBoxProps { - data: Conversation; + data: Conversation; } // Helper function to extract display name from User type -// Note: This is a module-level pure function. Memoization happens inside the component via useMemo. -const getUserDisplayName = (user: Conversation['sharer'] | Conversation['reserver'] | undefined): string => { - if (!user) return "Unknown"; - // Handle both PersonalUser and AdminUser account structures - const account = 'account' in user ? user.account : undefined; - const firstName = account?.profile?.firstName; - const lastName = account?.profile?.lastName; - if (firstName || lastName) { - return [firstName, lastName].filter(Boolean).join(" "); - } - return account?.username || "Unknown"; +// This is a module-level pure function that can be called directly. +const getUserDisplayName = ( + user: Conversation['sharer'] | Conversation['reserver'] | undefined, +): string => { + if (!user) return 'Unknown'; + // Handle both PersonalUser and AdminUser account structures + const account = 'account' in user ? user.account : undefined; + const firstName = account?.profile?.firstName; + const lastName = account?.profile?.lastName; + if (firstName || lastName) { + return [firstName, lastName].filter(Boolean).join(' '); + } + return account?.username || 'Unknown'; }; export const ConversationBox: React.FC = (props) => { - const [messageText, setMessageText] = useState(""); + const [messageText, setMessageText] = useState(''); - const currentUserId = props?.data?.sharer?.id; + const currentUserId = props?.data?.sharer?.id; - const handleSendMessage = useCallback((e: React.FormEvent) => { - e.preventDefault(); - console.log("Send message logic to be implemented", messageText); - }, [messageText]); + const handleSendMessage = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + console.log('Send message logic to be implemented', messageText); + }, + [messageText], + ); - // Build user info for sharer and reserver - const sharerInfo = props.data?.sharer ? { - id: props.data.sharer.id, - displayName: getUserDisplayName(props.data.sharer), - } : undefined; + // Build user info for sharer and reserver + const sharerInfo = props.data?.sharer + ? { + id: props.data.sharer.id, + displayName: getUserDisplayName(props.data.sharer), + } + : undefined; - const reserverInfo = props.data?.reserver ? { - id: props.data.reserver.id, - displayName: getUserDisplayName(props.data.reserver), - } : undefined; + const reserverInfo = props.data?.reserver + ? { + id: props.data.reserver.id, + displayName: getUserDisplayName(props.data.reserver), + } + : undefined; - return ( - <> -
- -
+ return ( + <> +
+ +
-
- -
- - ); +
+ +
+ + ); }; diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx index d06aca065..f6705035a 100644 --- a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx @@ -68,7 +68,7 @@ export const UserAvatar: React.FC = ({ }} icon={} > - {userName.charAt(0).toUpperCase()} + {userName?.trim() ? userName.charAt(0).toUpperCase() : '?'} ); From 599c4d08a1821141898e40d9e84d5b477250d899 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:22:49 +0000 Subject: [PATCH 20/42] refactor: centralize user profile routes and optimize conversation box memoization Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../docs/user-profile-navigation.md | 15 +++++++- .../messages/components/conversation-box.tsx | 36 +++++++++++-------- .../src/components/shared/user-avatar.tsx | 3 +- .../components/shared/user-profile-link.tsx | 3 +- .../components/shared/utils/user-routes.ts | 14 ++++++++ 5 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 apps/ui-sharethrift/src/components/shared/utils/user-routes.ts diff --git a/apps/ui-sharethrift/docs/user-profile-navigation.md b/apps/ui-sharethrift/docs/user-profile-navigation.md index fdbb49a19..d55c579a3 100644 --- a/apps/ui-sharethrift/docs/user-profile-navigation.md +++ b/apps/ui-sharethrift/docs/user-profile-navigation.md @@ -16,6 +16,19 @@ User profiles are accessed via the route: Where `:userId` is the ObjectID of the user. +### Route Helper Function + +To ensure consistency across the application and make it easier to update routes if the URL structure changes, use the centralized route helper: + +```tsx +import { getUserProfilePath } from '../shared/utils/user-routes.ts'; + +const profileUrl = getUserProfilePath(userId); +// Returns: "/user/507f1f77bcf86cd799439011" +``` + +The `UserProfileLink` and `UserAvatar` components automatically use this helper internally. + ## Shared Components Two reusable components are provided for consistent user profile navigation: @@ -146,7 +159,7 @@ type ListingRequest { Both `UserProfileLink` and `UserAvatar` components are designed with accessibility in mind: -- Use semantic Ant Design Link component for proper keyboard navigation +- Use the semantic Ant Design Link component for proper keyboard navigation - Include ARIA labels on avatars (`aria-label="View {userName}'s profile"`) - Maintain focus states for keyboard navigation - Provide visual indication of clickability (hover states, pointer cursor) diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx index 87bf0ae1a..be44f8cb3 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx @@ -1,7 +1,7 @@ import type { Conversation } from '../../../../../generated.tsx'; import { ListingBanner } from './listing-banner.tsx'; import { MessageThread } from './index.ts'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useMemo } from 'react'; interface ConversationBoxProps { data: Conversation; @@ -36,20 +36,28 @@ export const ConversationBox: React.FC = (props) => { [messageText], ); - // Build user info for sharer and reserver - const sharerInfo = props.data?.sharer - ? { - id: props.data.sharer.id, - displayName: getUserDisplayName(props.data.sharer), - } - : undefined; + // Build user info for sharer and reserver - memoized to avoid unnecessary rerenders + const sharerInfo = useMemo( + () => + props.data?.sharer + ? { + id: props.data.sharer.id, + displayName: getUserDisplayName(props.data.sharer), + } + : undefined, + [props.data?.sharer], + ); - const reserverInfo = props.data?.reserver - ? { - id: props.data.reserver.id, - displayName: getUserDisplayName(props.data.reserver), - } - : undefined; + const reserverInfo = useMemo( + () => + props.data?.reserver + ? { + id: props.data.reserver.id, + displayName: getUserDisplayName(props.data.reserver), + } + : undefined, + [props.data?.reserver], + ); return ( <> diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx index f6705035a..17afc90b3 100644 --- a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx @@ -1,6 +1,7 @@ import { Avatar as AntAvatar } from 'antd'; import { Link } from 'react-router-dom'; import { isValidUserId } from './utils/user-validation.ts'; +import { getUserProfilePath } from './utils/user-routes.ts'; import { ShareThriftLogo } from './sharethrift-logo.tsx'; /** @@ -78,7 +79,7 @@ export const UserAvatar: React.FC = ({ } return ( - + {avatarContent} ); diff --git a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx index 79b8f08e3..4bcf33418 100644 --- a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import { isValidUserId } from './utils/user-validation.ts'; +import { getUserProfilePath } from './utils/user-routes.ts'; /** * Props for UserProfileLink component @@ -39,7 +40,7 @@ export const UserProfileLink: React.FC = ({ return ( { + return `/user/${userId}`; +}; From 58fe3ba48c691d6cec717c79f2b6cae7d1c7e38e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:01:12 +0000 Subject: [PATCH 21/42] fix: add fallback for broken avatar images with onError handler Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../src/components/shared/user-avatar.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx index 17afc90b3..7bcf15e24 100644 --- a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Avatar as AntAvatar } from 'antd'; import { Link } from 'react-router-dom'; import { isValidUserId } from './utils/user-validation.ts'; @@ -42,8 +43,12 @@ export const UserAvatar: React.FC = ({ }) => { // Determine if we have a valid userId for linking const isClickable = isValidUserId(userId); + + // Track if the avatar image failed to load + const [imageError, setImageError] = useState(false); + const showImageAvatar = !!avatarUrl && !imageError; - const avatarContent = avatarUrl ? ( + const avatarContent = showImageAvatar ? ( = ({ className={className} style={{ ...style, cursor: isClickable ? 'pointer' : 'default' }} alt={`${userName}'s avatar`} + onError={() => { + // If the image fails to load, fall back to the logo/initials variant + setImageError(true); + return false; + }} /> ) : ( Date: Fri, 19 Dec 2025 12:06:51 +0530 Subject: [PATCH 22/42] fix: improve error handling in startServer function and update integration tests to use dynamic mock server URL --- .../messaging-service-mock/src/index.test.ts | 31 ++++++++++--------- .../mock-messaging-server/src/index.ts | 3 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/sthrift/messaging-service-mock/src/index.test.ts b/packages/sthrift/messaging-service-mock/src/index.test.ts index dda9dfb46..e7e62dd96 100644 --- a/packages/sthrift/messaging-service-mock/src/index.test.ts +++ b/packages/sthrift/messaging-service-mock/src/index.test.ts @@ -1,21 +1,24 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { Server } from 'node:http'; +import type { AddressInfo } from 'node:net'; import { ServiceMessagingMock } from './index.ts'; import { startServer, stopServer } from '@sthrift/mock-messaging-server'; describe('ServiceMessagingMock Integration Tests', () => { let service: ServiceMessagingMock; let mockServer: Server; + let mockServerUrl: string; const originalEnv = { ...process.env }; - const MOCK_SERVER_PORT = 10000; - const MOCK_SERVER_URL = `http://localhost:${MOCK_SERVER_PORT}`; + const MOCK_SERVER_HOST = 'http://localhost'; beforeAll(async () => { - process.env['MESSAGING_MOCK_URL'] = MOCK_SERVER_URL; - - mockServer = await startServer(MOCK_SERVER_PORT, true); - - await new Promise((resolve) => setTimeout(resolve, 500)); + mockServer = await startServer(0, true); + const address = mockServer.address(); + if (!address || typeof address === 'string') { + throw new Error('Mock server did not provide a TCP address'); + } + mockServerUrl = `${MOCK_SERVER_HOST}:${(address as AddressInfo).port}`; + process.env['MESSAGING_MOCK_URL'] = mockServerUrl; }, 15000); afterAll(async () => { @@ -27,14 +30,14 @@ describe('ServiceMessagingMock Integration Tests', () => { describe('Service Lifecycle', () => { it('should start up successfully in mock mode', async () => { - service = new ServiceMessagingMock(MOCK_SERVER_URL); + service = new ServiceMessagingMock(mockServerUrl); await service.startUp(); expect(service).toBeDefined(); await service.shutDown(); }); it('should throw error when starting up twice', async () => { - service = new ServiceMessagingMock(MOCK_SERVER_URL); + service = new ServiceMessagingMock(mockServerUrl); await service.startUp(); await expect(service.startUp()).rejects.toThrow( 'ServiceMessagingMock is already started', @@ -43,7 +46,7 @@ describe('ServiceMessagingMock Integration Tests', () => { }); it('should throw error when shutting down without starting', async () => { - service = new ServiceMessagingMock(MOCK_SERVER_URL); + service = new ServiceMessagingMock(mockServerUrl); await expect(service.shutDown()).rejects.toThrow( 'ServiceMessagingMock is not started - shutdown cannot proceed', ); @@ -54,7 +57,7 @@ describe('ServiceMessagingMock Integration Tests', () => { let conversationId: string; beforeAll(async () => { - service = new ServiceMessagingMock(MOCK_SERVER_URL); + service = new ServiceMessagingMock(mockServerUrl); await service.startUp(); }); @@ -116,7 +119,7 @@ describe('ServiceMessagingMock Integration Tests', () => { let createdConversationId: string; beforeAll(async () => { - service = new ServiceMessagingMock(MOCK_SERVER_URL); + service = new ServiceMessagingMock(mockServerUrl); await service.startUp(); }); @@ -150,7 +153,7 @@ describe('ServiceMessagingMock Integration Tests', () => { describe('End-to-End Workflow', () => { beforeAll(async () => { - service = new ServiceMessagingMock(MOCK_SERVER_URL); + service = new ServiceMessagingMock(mockServerUrl); await service.startUp(); }); @@ -198,7 +201,7 @@ describe('ServiceMessagingMock Integration Tests', () => { describe('Error Handling', () => { beforeAll(async () => { - service = new ServiceMessagingMock(MOCK_SERVER_URL); + service = new ServiceMessagingMock(mockServerUrl); await service.startUp(); }); diff --git a/packages/sthrift/mock-messaging-server/src/index.ts b/packages/sthrift/mock-messaging-server/src/index.ts index 93bc09228..0ae72ea80 100644 --- a/packages/sthrift/mock-messaging-server/src/index.ts +++ b/packages/sthrift/mock-messaging-server/src/index.ts @@ -67,7 +67,7 @@ export function createApp(): Application { } export function startServer(port = 10000, seedData = false): Promise { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const app = createApp(); const server = app.listen(port, () => { console.log(`Mock Twilio Server listening on port ${port}`); @@ -80,6 +80,7 @@ export function startServer(port = 10000, seedData = false): Promise { resolve(server); }); + server.on('error', reject); }); } From ff941e7bf8b74b1ba57c19e1c33bd74ba55edf62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:14:20 +0000 Subject: [PATCH 23/42] refactor: address code review feedback - remove unnecessary utilities and documentation Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../docs/user-profile-navigation.md | 191 ------------------ .../components/my-listings-dashboard.types.ts | 1 - .../requests-table.container.graphql | 1 - .../my-listings/components/requests-table.tsx | 16 +- .../src/components/shared/user-avatar.tsx | 23 +-- .../components/shared/user-profile-link.tsx | 17 +- .../components/shared/utils/user-routes.ts | 14 -- .../shared/utils/user-validation.ts | 14 -- .../schema/types/listing/item-listing.graphql | 1 - .../reservation-request.resolvers.ts | 2 - 10 files changed, 11 insertions(+), 269 deletions(-) delete mode 100644 apps/ui-sharethrift/docs/user-profile-navigation.md delete mode 100644 apps/ui-sharethrift/src/components/shared/utils/user-routes.ts delete mode 100644 apps/ui-sharethrift/src/components/shared/utils/user-validation.ts diff --git a/apps/ui-sharethrift/docs/user-profile-navigation.md b/apps/ui-sharethrift/docs/user-profile-navigation.md deleted file mode 100644 index d55c579a3..000000000 --- a/apps/ui-sharethrift/docs/user-profile-navigation.md +++ /dev/null @@ -1,191 +0,0 @@ -# User Profile Navigation - -This document describes the navigation pattern for linking to user profiles from various locations in the ShareThrift UI. - -## Overview - -Users can navigate to another user's profile by clicking on their name or profile image wherever it appears in the application. This provides a consistent and intuitive way to view user information. - -## Navigation Route - -User profiles are accessed via the route: - -``` -/user/:userId -``` - -Where `:userId` is the ObjectID of the user. - -### Route Helper Function - -To ensure consistency across the application and make it easier to update routes if the URL structure changes, use the centralized route helper: - -```tsx -import { getUserProfilePath } from '../shared/utils/user-routes.ts'; - -const profileUrl = getUserProfilePath(userId); -// Returns: "/user/507f1f77bcf86cd799439011" -``` - -The `UserProfileLink` and `UserAvatar` components automatically use this helper internally. - -## Shared Components - -Two reusable components are provided for consistent user profile navigation: - -### UserProfileLink - -A clickable text link that navigates to a user's profile. - -**Usage:** -```tsx -import { UserProfileLink } from '../shared/user-profile-link.tsx'; - - -``` - -**Props:** -- `userId` (required): The ObjectID of the user -- `displayName` (required): The text to display as the link -- `className` (optional): Additional CSS classes -- `style` (optional): Custom inline styles - -### UserAvatar - -A clickable avatar that navigates to a user's profile. - -**Usage:** -```tsx -import { UserAvatar } from '../shared/user-avatar.tsx'; - - -``` - -**Props:** -- `userId` (required): The ObjectID of the user -- `userName` (required): The name of the user (for accessibility and fallback initial) -- `size` (optional): Size of the avatar in pixels (default: 48) -- `avatarUrl` (optional): URL of the user's avatar image -- `className` (optional): Additional CSS classes -- `style` (optional): Custom inline styles -- `shape` (optional): 'circle' or 'square' (default: 'circle') - -## Implementation Locations - -User profile navigation has been implemented in the following components: - -### 1. Listing Sharer Information -**Component:** `sharer-information.tsx` -- Avatar and name link to the sharer's profile -- Located on item listing detail pages - -### 2. Conversation Headers (Listing Banner) -**Component:** `listing-banner.tsx` -- User name in conversation headers links to their profile -- Located in the Messages section - -### 3. My Listings - Requests Table -**Component:** `requests-table.tsx` -- "Requested By" column entries link to the requester's profile -- Located in My Listings > Requests view - -### 4. Message Thread -**Component:** `message-thread.tsx` -- Message author avatars link to their profiles -- Located in the Messages conversation view - -## GraphQL Integration - -### User Profile Query - -The `UserProfileViewContainer` uses the following GraphQL query to fetch user data: - -```graphql -query HomeViewUserProfileContainerUserById($userId: ObjectID!) { - userById(id: $userId) { - ... on PersonalUser { - id - userType - createdAt - account { - accountType - username - profile { - firstName - lastName - aboutMe - location { - city - state - } - } - } - } - ... on AdminUser { - # Similar fields for admin users - } - } -} -``` - -### Listing Request Enhancement - -The `ListingRequest` type has been enhanced with a `requestedById` field: - -```graphql -type ListingRequest { - id: ObjectID! - title: String! - image: String - requestedBy: String! - requestedById: ObjectID # New field - requestedOn: String! - reservationPeriod: String! - status: String! -} -``` - -## Accessibility - -Both `UserProfileLink` and `UserAvatar` components are designed with accessibility in mind: - -- Use the semantic Ant Design Link component for proper keyboard navigation -- Include ARIA labels on avatars (`aria-label="View {userName}'s profile"`) -- Maintain focus states for keyboard navigation -- Provide visual indication of clickability (hover states, pointer cursor) - -## Best Practices - -1. **Always use the shared components** (`UserProfileLink` and `UserAvatar`) instead of creating custom navigation implementations -2. **Pass the user ID** from your GraphQL data to ensure correct navigation -3. **Provide meaningful display names** for accessibility -4. **Test keyboard navigation** when implementing in new locations -5. **Handle edge cases** (e.g., when user data is unavailable) - -## Testing - -Storybook stories are available for both components: -- `UserProfileLink.stories.tsx` -- `UserAvatar.stories.tsx` - -These stories demonstrate various use cases and styling options. - -## Future Enhancements - -Potential future enhancements to the user profile navigation pattern: - -1. Add user listings to the public profile view (requires backend query for public listings by user ID) -2. Implement user blocking/reporting functionality accessible from profile -3. Add user rating/review system -4. Enable direct messaging from profile view -5. Show mutual connections or shared listings diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts index ef3a7bdfe..8aaadd577 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts @@ -13,7 +13,6 @@ export interface ListingRequestData { title: string; image?: string | null; requestedBy: string; - requestedById?: string | null; requestedOn: string; reservationPeriod: string; status: string; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.container.graphql b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.container.graphql index 352b7f773..72dd5f98b 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.container.graphql +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.container.graphql @@ -28,7 +28,6 @@ fragment HomeRequestsTableContainerRequestFields on ListingRequest { title image requestedBy - requestedById requestedOn reservationPeriod status diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx index b79f24846..767563e65 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx @@ -8,7 +8,6 @@ import { getStatusTagClass, getActionButtons, } from './requests-status-helpers.tsx'; -import { UserProfileLink } from '../../../../shared/user-profile-link.tsx'; const { Search } = Input; @@ -108,18 +107,9 @@ export const RequestsTable: React.FC = ({ key: 'requestedBy', sorter: true, sortOrder: sorter.field === 'requestedBy' ? sorter.order : null, - render: (username: string, record: ListingRequestData) => { - if (record.requestedById) { - return ( - - ); - } - return {username}; - }, + render: (username: string) => ( + {username} + ), }, { title: 'Requested On', diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx index 7bcf15e24..3e4da42bd 100644 --- a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx @@ -1,8 +1,6 @@ import { useState } from 'react'; import { Avatar as AntAvatar } from 'antd'; import { Link } from 'react-router-dom'; -import { isValidUserId } from './utils/user-validation.ts'; -import { getUserProfilePath } from './utils/user-routes.ts'; import { ShareThriftLogo } from './sharethrift-logo.tsx'; /** @@ -26,11 +24,10 @@ interface UserAvatarProps { } /** - * UserAvatar component displays a user's avatar that optionally links to their profile. - * When a valid userId is provided, clicking the avatar navigates to the user's profile. - * Falls back to a non-clickable avatar when userId is missing or invalid. + * UserAvatar component displays a user's avatar that links to their profile. + * Clicking the avatar navigates to the user's profile page. * @param props - The component props - * @returns JSX element containing the avatar, optionally wrapped in a link + * @returns JSX element containing the avatar wrapped in a link */ export const UserAvatar: React.FC = ({ userId, @@ -41,9 +38,6 @@ export const UserAvatar: React.FC = ({ style = {}, shape = 'circle', }) => { - // Determine if we have a valid userId for linking - const isClickable = isValidUserId(userId); - // Track if the avatar image failed to load const [imageError, setImageError] = useState(false); const showImageAvatar = !!avatarUrl && !imageError; @@ -54,7 +48,7 @@ export const UserAvatar: React.FC = ({ src={avatarUrl} shape={shape} className={className} - style={{ ...style, cursor: isClickable ? 'pointer' : 'default' }} + style={{ ...style, cursor: 'pointer' }} alt={`${userName}'s avatar`} onError={() => { // If the image fails to load, fall back to the logo/initials variant @@ -74,7 +68,7 @@ export const UserAvatar: React.FC = ({ justifyContent: 'center', flexShrink: 0, fontFamily: 'var(--Urbanist, Arial, sans-serif)', - cursor: isClickable ? 'pointer' : 'default', + cursor: 'pointer', ...style, }} icon={} @@ -83,13 +77,8 @@ export const UserAvatar: React.FC = ({ ); - // If no valid userId, render avatar without link - if (!isClickable) { - return avatarContent; - } - return ( - + {avatarContent} ); diff --git a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx index 4bcf33418..4b7126f76 100644 --- a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx @@ -1,6 +1,4 @@ import { Link } from 'react-router-dom'; -import { isValidUserId } from './utils/user-validation.ts'; -import { getUserProfilePath } from './utils/user-routes.ts'; /** * Props for UserProfileLink component @@ -18,10 +16,8 @@ interface UserProfileLinkProps { /** * UserProfileLink component renders a clickable link to a user's profile. - * When a valid userId is provided, renders a navigation link. - * Falls back to plain text when userId is missing or invalid. * @param props - The component props - * @returns JSX element containing either a link or plain text + * @returns JSX element containing a navigation link */ export const UserProfileLink: React.FC = ({ userId, @@ -29,18 +25,9 @@ export const UserProfileLink: React.FC = ({ className = '', style = {}, }) => { - // If no valid userId, render as plain text instead of a broken link - if (!isValidUserId(userId)) { - return ( - - {displayName} - - ); - } - return ( { - return `/user/${userId}`; -}; diff --git a/apps/ui-sharethrift/src/components/shared/utils/user-validation.ts b/apps/ui-sharethrift/src/components/shared/utils/user-validation.ts deleted file mode 100644 index 89c763ae7..000000000 --- a/apps/ui-sharethrift/src/components/shared/utils/user-validation.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Utility functions for user-related validations - */ - -/** - * Checks if a userId string is present and non-empty. - * This is a basic check for routing purposes - it does not validate the format or existence - * of the user in the database. Invalid IDs are handled gracefully by the GraphQL layer. - * @param userId - The user ID to check - * @returns true if the userId is a non-empty string, false otherwise - */ -export const isValidUserId = (userId: string | undefined | null): boolean => { - return !!userId && userId.trim() !== ''; -}; diff --git a/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql b/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql index 6ae9e0c40..3d7ce0a81 100644 --- a/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql +++ b/packages/sthrift/graphql/src/schema/types/listing/item-listing.graphql @@ -48,7 +48,6 @@ type ListingRequest { title: String! image: String requestedBy: String! - requestedById: ObjectID requestedOn: String! reservationPeriod: String! status: String! diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts index 2106c99ae..a1651e377 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts @@ -22,7 +22,6 @@ interface ListingRequestUiShape { title: string; image: string; requestedBy: string; - requestedById?: string | null; requestedOn: string; reservationPeriod: string; status: string; @@ -60,7 +59,6 @@ export function paginateAndFilterListingRequests( requestedBy: r.reserver?.account?.username ? `@${r.reserver.account.username}` : '@unknown', - requestedById: r.reserver?.id ?? null, requestedOn: r.createdAt instanceof Date ? r.createdAt.toISOString() From a2808c57a4eb83201446f50c6c4d3c0856a796b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:40:41 +0000 Subject: [PATCH 24/42] refactor: remove unnecessary userId guard in UserProfileViewContainer Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../view-user-profile/user-profile-view.container.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx index 5b7af000f..bec09113f 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx @@ -61,10 +61,6 @@ export const UserProfileViewContainer: React.FC = () => { // This would require a backend query to fetch public listings by user ID. const listings: ItemListing[] = []; - if (!userId) { - return
User ID is required
; - } - return ( Date: Wed, 24 Dec 2025 16:53:27 +0000 Subject: [PATCH 25/42] refactor: clarify required MessageThread props and remove unnecessary optionality Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../messages/components/conversation-box.tsx | 24 +++++++------------ .../messages/components/message-thread.tsx | 8 +++---- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx index be44f8cb3..5ad640517 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx @@ -26,7 +26,7 @@ const getUserDisplayName = ( export const ConversationBox: React.FC = (props) => { const [messageText, setMessageText] = useState(''); - const currentUserId = props?.data?.sharer?.id; + const currentUserId = props?.data?.sharer?.id || ''; const handleSendMessage = useCallback( (e: React.FormEvent) => { @@ -38,24 +38,18 @@ export const ConversationBox: React.FC = (props) => { // Build user info for sharer and reserver - memoized to avoid unnecessary rerenders const sharerInfo = useMemo( - () => - props.data?.sharer - ? { - id: props.data.sharer.id, - displayName: getUserDisplayName(props.data.sharer), - } - : undefined, + () => ({ + id: props.data.sharer?.id || '', + displayName: getUserDisplayName(props.data.sharer), + }), [props.data?.sharer], ); const reserverInfo = useMemo( - () => - props.data?.reserver - ? { - id: props.data.reserver.id, - displayName: getUserDisplayName(props.data.reserver), - } - : undefined, + () => ({ + id: props.data.reserver?.id || '', + displayName: getUserDisplayName(props.data.reserver), + }), [props.data?.reserver], ); diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx index 8d076f821..a3cb5130e 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/message-thread.tsx @@ -27,8 +27,8 @@ interface MessageThreadProps { handleSendMessage: (e: React.FormEvent) => void; currentUserId: string; contentContainerStyle?: React.CSSProperties; - sharer?: UserInfo; - reserver?: UserInfo; + sharer: UserInfo; + reserver: UserInfo; } export const MessageThread: React.FC = (props) => { @@ -36,10 +36,10 @@ export const MessageThread: React.FC = (props) => { // Helper to get display name for a given authorId - memoized for performance const getAuthorDisplayName = useCallback((authorId: string): string => { - if (props.sharer?.id === authorId) { + if (props.sharer.id === authorId) { return props.sharer.displayName || "Sharer"; } - if (props.reserver?.id === authorId) { + if (props.reserver.id === authorId) { return props.reserver.displayName || "Reserver"; } return "User"; From 0a6e235910f767daa27e4b4da3a3f0a03cfddbac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 16:58:06 +0000 Subject: [PATCH 26/42] refactor: remove defensive null check for reservation period field Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../layouts/home/my-listings/components/requests-table.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx index 767563e65..f7e1b536f 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.tsx @@ -126,11 +126,7 @@ export const RequestsTable: React.FC = ({ sorter: true, sortOrder: sorter.field === 'reservationPeriod' ? sorter.order : null, render: (period: string) => { - if (!period) { - return 'N/A'; - } // Expect format 'yyyy-mm-dd - yyyy-mm-dd' or similar - // If not, try to parse and format let start = '', end = ''; if (period.includes(' - ')) { @@ -140,7 +136,7 @@ export const RequestsTable: React.FC = ({ } else { start = period ?? ''; } - // Try to format both as yyyy-mm-dd + // Format both dates as yyyy-mm-dd function formatDate(str: string) { const d = new Date(str); if (isNaN(d.getTime())) { From 267274853ee913c6adb085a96f435f7d3023e02a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 18:28:44 +0000 Subject: [PATCH 27/42] refactor: remove redundant userId defensive checks in listing-banner Co-authored-by: dani-vaibhav <182140623+dani-vaibhav@users.noreply.github.com> --- .../layouts/home/messages/components/listing-banner.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/listing-banner.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/listing-banner.tsx index 3d32d2e99..92ce62235 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/listing-banner.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/listing-banner.tsx @@ -15,7 +15,7 @@ export const ListingBanner: React.FC = (props) => { const status = "Request Submitted"; //todo const imageUrl = imgRectangle26; //todo - const firstName = props.owner?.account?.profile?.firstName || 'Unknown'; return ( + const firstName = props.owner.account?.profile?.firstName || props.owner.account?.username || 'Unknown'; return ( = (props) => { }} > Date: Mon, 29 Dec 2025 23:27:03 +0530 Subject: [PATCH 28/42] feat: add requestedById field to ListingRequestUiShape and update pagination logic --- packages/sthrift/graphql/src/helpers/tracing.test.ts | 11 +++-------- .../reservation-request.resolvers.ts | 2 ++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/sthrift/graphql/src/helpers/tracing.test.ts b/packages/sthrift/graphql/src/helpers/tracing.test.ts index 2e191b6ae..f063e022b 100644 --- a/packages/sthrift/graphql/src/helpers/tracing.test.ts +++ b/packages/sthrift/graphql/src/helpers/tracing.test.ts @@ -46,15 +46,10 @@ vi.mock('@opentelemetry/api', async (importOriginal) => { fn: (span: Span) => Promise, ): Promise => { const mockSpan = createMockSpan(); - const globals = global as unknown as { - __mockSpan?: Span; - __mockTracerName?: string; - __mockSpanName?: string; - }; // Store references for assertions - globals.__mockSpan = mockSpan; - globals.__mockTracerName = tracerName; - globals.__mockSpanName = spanName; + (global as Record)['__mockSpan'] = mockSpan; + (global as Record)['__mockTracerName'] = tracerName; + (global as Record)['__mockSpanName'] = spanName; return fn(mockSpan); }, ), diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts index a1651e377..28ab4d15e 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts @@ -22,6 +22,7 @@ interface ListingRequestUiShape { title: string; image: string; requestedBy: string; + requestedById: string | null; requestedOn: string; reservationPeriod: string; status: string; @@ -59,6 +60,7 @@ export function paginateAndFilterListingRequests( requestedBy: r.reserver?.account?.username ? `@${r.reserver.account.username}` : '@unknown', + requestedById: r.reserver?.id ?? null, requestedOn: r.createdAt instanceof Date ? r.createdAt.toISOString() From dc250e01f7fab454f1fb64746fafb94bb1f78331 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Mon, 29 Dec 2025 23:33:27 +0530 Subject: [PATCH 29/42] revert: restore original messaging-service-mock test logic (remove unnecessary Copilot changes) --- .../messaging-service-mock/src/index.test.ts | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/sthrift/messaging-service-mock/src/index.test.ts b/packages/sthrift/messaging-service-mock/src/index.test.ts index e7e62dd96..1d98192f5 100644 --- a/packages/sthrift/messaging-service-mock/src/index.test.ts +++ b/packages/sthrift/messaging-service-mock/src/index.test.ts @@ -1,24 +1,21 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import type { Server } from 'node:http'; -import type { AddressInfo } from 'node:net'; import { ServiceMessagingMock } from './index.ts'; import { startServer, stopServer } from '@sthrift/mock-messaging-server'; describe('ServiceMessagingMock Integration Tests', () => { let service: ServiceMessagingMock; let mockServer: Server; - let mockServerUrl: string; const originalEnv = { ...process.env }; - const MOCK_SERVER_HOST = 'http://localhost'; + const MOCK_SERVER_PORT = 10000; + const MOCK_SERVER_URL = `http://localhost:${MOCK_SERVER_PORT}`; beforeAll(async () => { - mockServer = await startServer(0, true); - const address = mockServer.address(); - if (!address || typeof address === 'string') { - throw new Error('Mock server did not provide a TCP address'); - } - mockServerUrl = `${MOCK_SERVER_HOST}:${(address as AddressInfo).port}`; - process.env['MESSAGING_MOCK_URL'] = mockServerUrl; + process.env['MESSAGING_MOCK_URL'] = MOCK_SERVER_URL; + + mockServer = await startServer(MOCK_SERVER_PORT, true); + + await new Promise((resolve) => setTimeout(resolve, 500)); }, 15000); afterAll(async () => { @@ -30,14 +27,14 @@ describe('ServiceMessagingMock Integration Tests', () => { describe('Service Lifecycle', () => { it('should start up successfully in mock mode', async () => { - service = new ServiceMessagingMock(mockServerUrl); + service = new ServiceMessagingMock(MOCK_SERVER_URL); await service.startUp(); expect(service).toBeDefined(); await service.shutDown(); }); it('should throw error when starting up twice', async () => { - service = new ServiceMessagingMock(mockServerUrl); + service = new ServiceMessagingMock(MOCK_SERVER_URL); await service.startUp(); await expect(service.startUp()).rejects.toThrow( 'ServiceMessagingMock is already started', @@ -46,7 +43,7 @@ describe('ServiceMessagingMock Integration Tests', () => { }); it('should throw error when shutting down without starting', async () => { - service = new ServiceMessagingMock(mockServerUrl); + service = new ServiceMessagingMock(MOCK_SERVER_URL); await expect(service.shutDown()).rejects.toThrow( 'ServiceMessagingMock is not started - shutdown cannot proceed', ); @@ -57,7 +54,7 @@ describe('ServiceMessagingMock Integration Tests', () => { let conversationId: string; beforeAll(async () => { - service = new ServiceMessagingMock(mockServerUrl); + service = new ServiceMessagingMock(MOCK_SERVER_URL); await service.startUp(); }); @@ -119,7 +116,7 @@ describe('ServiceMessagingMock Integration Tests', () => { let createdConversationId: string; beforeAll(async () => { - service = new ServiceMessagingMock(mockServerUrl); + service = new ServiceMessagingMock(MOCK_SERVER_URL); await service.startUp(); }); @@ -153,7 +150,7 @@ describe('ServiceMessagingMock Integration Tests', () => { describe('End-to-End Workflow', () => { beforeAll(async () => { - service = new ServiceMessagingMock(mockServerUrl); + service = new ServiceMessagingMock(MOCK_SERVER_URL); await service.startUp(); }); @@ -201,7 +198,7 @@ describe('ServiceMessagingMock Integration Tests', () => { describe('Error Handling', () => { beforeAll(async () => { - service = new ServiceMessagingMock(mockServerUrl); + service = new ServiceMessagingMock(MOCK_SERVER_URL); await service.startUp(); }); From 15248453ead8662a84d3039958bdc891b943aa7c Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 30 Dec 2025 00:37:20 +0530 Subject: [PATCH 30/42] feat: add requestedById field to ListingRequestData and implement user ID validation logic --- .../components/my-listings-dashboard.types.ts | 1 + .../components/shared/utils/user-validation.stories.tsx | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts index 8aaadd577..ef3a7bdfe 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts @@ -13,6 +13,7 @@ export interface ListingRequestData { title: string; image?: string | null; requestedBy: string; + requestedById?: string | null; requestedOn: string; reservationPeriod: string; status: string; diff --git a/apps/ui-sharethrift/src/components/shared/utils/user-validation.stories.tsx b/apps/ui-sharethrift/src/components/shared/utils/user-validation.stories.tsx index 2e15dd4e3..0e5703a10 100644 --- a/apps/ui-sharethrift/src/components/shared/utils/user-validation.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/utils/user-validation.stories.tsx @@ -1,6 +1,13 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect } from 'storybook/test'; -import { isValidUserId } from './user-validation.ts'; + +const isValidUserId = (userId: string | undefined | null): boolean => { + if (typeof userId !== 'string') { + return false; + } + + return userId.trim().length > 0; +}; const meta: Meta = { title: 'Shared/Utils/UserValidation', From 0226ec97982408af31a47092419895276729d218 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 30 Dec 2025 01:02:02 +0530 Subject: [PATCH 31/42] feat: enhance user profile link component to handle optional userId and improve fallback rendering --- .../stories/ListingBanner.stories.tsx | 2 +- .../components/requests-table.stories.tsx | 3 +-- .../components/shared/user-profile-link.tsx | 23 +++++++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx index 96af53ff3..cc5ccd286 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/stories/ListingBanner.stories.tsx @@ -87,6 +87,6 @@ export const UnknownOwner: Story = { const canvas = within(canvasElement); // Test fallback for missing profile - await expect(canvas.getByText("Unknown's Listing")).toBeInTheDocument(); + await expect(canvas.getByText("unknown_user's Listing")).toBeInTheDocument(); }, }; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx index a0863ea5e..f3c0d6cc7 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx @@ -63,8 +63,7 @@ export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await expect(canvas.getAllByText('Cordless Drill').length).toBeGreaterThan(0); - const link = canvas.getByRole('link', { name: '@alice' }); - await expect(link).toHaveAttribute('href', '/user/507f1f77bcf86cd799439011'); + await expect(canvas.getAllByText('@alice').length).toBeGreaterThan(0); await expect(canvas.getAllByText('Accepted').length).toBeGreaterThan(0); }, }; diff --git a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx index 4b7126f76..0560cc0bd 100644 --- a/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-profile-link.tsx @@ -8,7 +8,7 @@ import { Link } from 'react-router-dom'; * @property style - Custom inline styles */ interface UserProfileLinkProps { - userId: string; + userId?: string | null; displayName: string; className?: string; style?: React.CSSProperties; @@ -25,11 +25,26 @@ export const UserProfileLink: React.FC = ({ className = '', style = {}, }) => { + if (!userId?.trim()) { + return ( + + {displayName} + + ); + } + return ( - Date: Tue, 30 Dec 2025 12:01:39 +0530 Subject: [PATCH 32/42] fix: render missing userId message in public profile view --- .../user-profile-view.container.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx index bec09113f..0903abb0f 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-user-profile/user-profile-view.container.tsx @@ -33,6 +33,8 @@ export const UserProfileViewContainer: React.FC = () => { navigate(`/listing/${listingId}`); }, [navigate]); + const handleEditSettings = useCallback(() => undefined, []); + // Build profile user data from the query response - memoized for performance const profileUser = useMemo(() => { if (!viewedUser) return null; @@ -61,20 +63,28 @@ export const UserProfileViewContainer: React.FC = () => { // This would require a backend query to fetch public listings by user ID. const listings: ItemListing[] = []; + const profileView = profileUser ? ( + + ) : ( + <> + ); + + if (!userId) { + return
User ID is required
; + } + return ( {}} - onListingClick={handleListingClick} - /> - } + hasDataComponent={profileView} /> ); }; From 0c27758be6bd1db6b482cf79582bfab863e1448a Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 6 Jan 2026 00:26:17 +0530 Subject: [PATCH 33/42] refactor: extract SharerInformation container and move query/mutation logic --- .../sharer-information.container.tsx | 81 +++++++++++++++- .../sharer-information.stories.tsx | 96 ++----------------- .../sharer-information/sharer-information.tsx | 81 ++-------------- 3 files changed, 92 insertions(+), 166 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.container.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.container.tsx index d27e33e07..48194c96b 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.container.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.container.tsx @@ -1,6 +1,16 @@ -import { useQuery } from '@apollo/client/react'; +import { useMutation, useQuery } from '@apollo/client/react'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { SharerInformation } from './sharer-information.tsx'; -import { SharerInformationContainerDocument } from '../../../../../../generated.tsx'; +import { + CreateConversationDocument, + HomeConversationListContainerConversationsByUserDocument, + SharerInformationContainerDocument, +} from '../../../../../../generated.tsx'; +import type { + CreateConversationMutation, + CreateConversationMutationVariables, +} from '../../../../../../generated.tsx'; interface SharerInformationContainerProps { sharerId: string; @@ -15,6 +25,9 @@ interface SharerInformationContainerProps { export const SharerInformationContainer: React.FC< SharerInformationContainerProps > = ({ sharerId, listingId, isOwner, sharedTimeAgo, className, currentUserId }) => { + const [isMobile, setIsMobile] = useState(false); + const navigate = useNavigate(); + const { data, loading, error } = useQuery( SharerInformationContainerDocument, { @@ -22,6 +35,64 @@ export const SharerInformationContainer: React.FC< }, ); + const [createConversation, { loading: isCreating }] = useMutation< + CreateConversationMutation, + CreateConversationMutationVariables + >(CreateConversationDocument, { + refetchQueries: [ + { + query: HomeConversationListContainerConversationsByUserDocument, + variables: { userId: currentUserId }, + }, + ], + awaitRefetchQueries: true, + onCompleted: (data) => { + if (data.createConversation.status.success) { + navigate('/messages', { + state: { + selectedConversationId: data.createConversation.conversation?.id, + }, + replace: false, + }); + } else { + console.log( + 'Failed to create conversation:', + data.createConversation.status.errorMessage, + ); + } + }, + onError: (error) => { + console.error('Error creating conversation:', error); + }, + }); + + const handleMessageSharer = async (resolvedSharerId: string) => { + if (!currentUserId) { + return; + } + + try { + await createConversation({ + variables: { + input: { + listingId, + sharerId: resolvedSharerId, + reserverId: currentUserId, + }, + }, + }); + } catch (error) { + console.error('Failed to create conversation:', error); + } + }; + + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth <= 600); + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + // If sharerId looks like a name (contains spaces or letters), use it directly // Otherwise, try to query for user data const isNameOnly = @@ -41,6 +112,9 @@ export const SharerInformationContainer: React.FC< sharedTimeAgo={sharedTimeAgo} className={className} currentUserId={currentUserId} + isCreating={isCreating} + isMobile={isMobile} + onMessageSharer={() => handleMessageSharer(sharer.id)} /> ); } @@ -66,6 +140,9 @@ export const SharerInformationContainer: React.FC< sharedTimeAgo={sharedTimeAgo} className={className} currentUserId={currentUserId} + isCreating={isCreating} + isMobile={isMobile} + onMessageSharer={() => handleMessageSharer(sharer.id)} /> ); }; diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.stories.tsx index 3772aa7e2..3d0aacad1 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.stories.tsx @@ -1,14 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect, within, userEvent } from 'storybook/test'; import { SharerInformation } from './sharer-information.tsx'; -import { - withMockApolloClient, - withMockRouter, -} from '../../../../../../test-utils/storybook-decorators.tsx'; -import { - CreateConversationDocument, - HomeConversationListContainerConversationsByUserDocument, -} from '../../../../../../generated.tsx'; const mockSharer = { id: 'user-1', @@ -21,50 +13,16 @@ const meta: Meta = { component: SharerInformation, parameters: { layout: 'padded', - apolloClient: { - mocks: [ - { - request: { - query: CreateConversationDocument, - variables: { - input: { - listingId: '1', - sharerId: 'user-1', - reserverId: 'user-2', - }, - }, - }, - result: { - data: { - createConversation: { - __typename: 'ConversationMutationResult', - status: { success: true, errorMessage: null }, - conversation: { __typename: 'Conversation', id: 'conv-1' }, - }, - }, - }, - }, - { - request: { - query: HomeConversationListContainerConversationsByUserDocument, - variables: { userId: 'user-2' }, - }, - result: { - data: { - conversationsByUser: [], - }, - }, - }, - ], - }, }, - decorators: [withMockApolloClient, withMockRouter('/listing/1')], args: { sharer: mockSharer, listingId: '1', isOwner: false, sharedTimeAgo: '2 days ago', currentUserId: 'user-2', + isCreating: false, + isMobile: false, + onMessageSharer: () => undefined, }, }; @@ -125,52 +83,10 @@ export const ClickMessageButton: Story = { }, }; -export const MessageButtonWithError: Story = { - parameters: { - apolloClient: { - mocks: [ - { - request: { - query: CreateConversationDocument, - variables: () => true, - }, - maxUsageCount: Number.POSITIVE_INFINITY, - result: { - data: { - createConversation: { - __typename: 'ConversationMutationResult', - status: { success: false, errorMessage: 'Failed to create' }, - conversation: null, - }, - }, - }, - }, - { - request: { - query: HomeConversationListContainerConversationsByUserDocument, - variables: () => true, - }, - maxUsageCount: Number.POSITIVE_INFINITY, - result: { - data: { - conversationsByUser: [], - }, - }, - }, - ], - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await expect(canvasElement).toBeTruthy(); - const messageButton = canvas.queryByRole('button', { name: /Message/i }); - if (messageButton) { - await userEvent.click(messageButton); - } - }, -}; - export const MobileView: Story = { + args: { + isMobile: true, + }, parameters: { viewport: { defaultViewport: 'mobile1', diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx index cd80d1aad..0d7f6f96d 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.tsx @@ -1,16 +1,5 @@ import { Button, Row, Col } from "antd"; -import { useEffect, useState } from "react"; import { MessageOutlined } from "@ant-design/icons"; -import { useMutation } from "@apollo/client/react"; -import { useNavigate } from "react-router-dom"; -import { - CreateConversationDocument, - HomeConversationListContainerConversationsByUserDocument, -} from "../../../../../../generated.tsx"; -import type { - CreateConversationMutation, - CreateConversationMutationVariables, -} from "../../../../../../generated.tsx"; import { UserAvatar } from "../../../../../shared/user-avatar.tsx"; import { UserProfileLink } from "../../../../../shared/user-profile-link.tsx"; @@ -27,77 +16,21 @@ interface SharerInformationProps { sharedTimeAgo?: string; className?: string; currentUserId?: string | null; + isCreating?: boolean; + isMobile?: boolean; + onMessageSharer?: () => void; } export const SharerInformation: React.FC = ({ sharer, - listingId, isOwner = false, sharedTimeAgo = "2 days ago", className = "", currentUserId, + isCreating = false, + isMobile = false, + onMessageSharer, }) => { - const [isMobile, setIsMobile] = useState(false); - const navigate = useNavigate(); - - const [createConversation, { loading: isCreating }] = useMutation< - CreateConversationMutation, - CreateConversationMutationVariables - >(CreateConversationDocument, { - refetchQueries: [ - { - query: HomeConversationListContainerConversationsByUserDocument, - variables: { userId: currentUserId }, - }, - ], - awaitRefetchQueries: true, - onCompleted: (data) => { - if (data.createConversation.status.success) { - navigate("/messages", { - state: { - selectedConversationId: data.createConversation.conversation?.id, - }, - replace: false, - }); - } else { - console.log( - "Failed to create conversation:", - data.createConversation.status.errorMessage - ); - } - }, - onError: (error) => { - console.error("Error creating conversation:", error); - }, - }); - - const handleMessageSharer = async () => { - if (!currentUserId) { - return; - } - - try { - await createConversation({ - variables: { - input: { - listingId, - sharerId: sharer.id, - reserverId: currentUserId, - }, - }, - }); - } catch (error) { - console.error("Failed to create conversation:", error); - } - }; - - useEffect(() => { - const checkMobile = () => setIsMobile(window.innerWidth <= 600); - checkMobile(); - window.addEventListener("resize", checkMobile); - return () => window.removeEventListener("resize", checkMobile); - }, []); - return ( = ({ type="default" icon={} loading={isCreating} - onClick={handleMessageSharer} + onClick={onMessageSharer} > {!isMobile && "Message Sharer"} From 13742b6778587f821f67b2bafc81d2a49332bcc7 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 6 Jan 2026 00:59:16 +0530 Subject: [PATCH 34/42] feat: add navigation to user profile in UserAvatar component --- .../sharer-information.stories.tsx | 2 ++ .../components/shared/user-avatar.stories.tsx | 27 ++++++++++++++++--- .../src/components/shared/user-avatar.tsx | 13 +++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.stories.tsx index 3d0aacad1..d5f8a61af 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/components/view-listing/sharer-information/sharer-information.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { expect, within, userEvent } from 'storybook/test'; import { SharerInformation } from './sharer-information.tsx'; +import { withMockRouter } from '../../../../../../test-utils/storybook-decorators.tsx'; const mockSharer = { id: 'user-1', @@ -14,6 +15,7 @@ const meta: Meta = { parameters: { layout: 'padded', }, + decorators: [withMockRouter('/listing/1')], args: { sharer: mockSharer, listingId: '1', diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx index fbaa654c5..be1096692 100644 --- a/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.stories.tsx @@ -1,15 +1,21 @@ import type { Meta, StoryObj } from '@storybook/react'; import { BrowserRouter } from 'react-router-dom'; import { UserAvatar } from './user-avatar.tsx'; +import { + MockAuthWrapper, + MockUnauthWrapper, +} from '../../test-utils/storybook-decorators.tsx'; const meta: Meta = { title: 'Shared/UserAvatar', component: UserAvatar, decorators: [ (Story) => ( - - - + + + + + ), ], tags: ['autodocs'], @@ -49,6 +55,21 @@ export const Default: Story = { }, }; +export const UnauthenticatedRedirectToLogin: Story = { + decorators: [ + (Story) => ( + + + + ), + ], + args: { + userId: '507f1f77bcf86cd799439011', + userName: 'John Doe', + size: 48, + }, +}; + export const Small: Story = { args: { userId: '507f1f77bcf86cd799439011', diff --git a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx index 3e4da42bd..71d7d2b86 100644 --- a/apps/ui-sharethrift/src/components/shared/user-avatar.tsx +++ b/apps/ui-sharethrift/src/components/shared/user-avatar.tsx @@ -1,6 +1,7 @@ -import { useState } from 'react'; +import { useContext, useState } from 'react'; import { Avatar as AntAvatar } from 'antd'; import { Link } from 'react-router-dom'; +import { AuthContext } from 'react-oidc-context'; import { ShareThriftLogo } from './sharethrift-logo.tsx'; /** @@ -38,6 +39,10 @@ export const UserAvatar: React.FC = ({ style = {}, shape = 'circle', }) => { + const auth = useContext(AuthContext); + const isAuthenticated = auth?.isAuthenticated ?? false; + const profilePath = userId ? (isAuthenticated ? `/user/${userId}` : '/login') : undefined; + // Track if the avatar image failed to load const [imageError, setImageError] = useState(false); const showImageAvatar = !!avatarUrl && !imageError; @@ -77,8 +82,12 @@ export const UserAvatar: React.FC = ({
); + if (!profilePath) { + return avatarContent; + } + return ( - + {avatarContent} ); From eb4aa96a356994d2247359c6b6685a55da8608be Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 6 Jan 2026 01:07:27 +0530 Subject: [PATCH 35/42] style: reformat HomeRoutes to match project formatting rules --- .../src/components/layouts/home/index.tsx | 142 +++++++++--------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/index.tsx b/apps/ui-sharethrift/src/components/layouts/home/index.tsx index a2fbe655a..667791899 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/index.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/index.tsx @@ -1,74 +1,74 @@ -import { Route, Routes } from "react-router-dom"; -import { AccountRoutes } from "./account/index.tsx"; -import { MessagesRoutes } from "./messages/Index.tsx"; -import { MyListingsRoutes } from "./my-listings/Index.tsx"; -import { MyReservationsRoutes } from "./my-reservations/Index.tsx"; -import { Listings } from "./pages/all-listings-page.tsx"; -import { ViewListing } from "./pages/view-listing-page.tsx"; -import { CreateListing } from "./pages/create-listing-page.tsx"; -import { ViewUserProfile } from "./pages/view-user-profile-page.tsx"; -import { SectionLayout } from "./section-layout.tsx"; -import { AdminDashboardMain } from "./account/admin-dashboard/pages/admin-dashboard-main.tsx"; -import { RequireAuth } from "../../shared/require-auth.tsx"; -import { RequireAuthAdmin } from "../../shared/require-auth-admin.tsx"; +import { Route, Routes } from 'react-router-dom'; +import { AccountRoutes } from './account/index.tsx'; +import { MessagesRoutes } from './messages/Index.tsx'; +import { MyListingsRoutes } from './my-listings/Index.tsx'; +import { MyReservationsRoutes } from './my-reservations/Index.tsx'; +import { Listings } from './pages/all-listings-page.tsx'; +import { ViewListing } from './pages/view-listing-page.tsx'; +import { CreateListing } from './pages/create-listing-page.tsx'; +import { ViewUserProfile } from './pages/view-user-profile-page.tsx'; +import { SectionLayout } from './section-layout.tsx'; +import { AdminDashboardMain } from './account/admin-dashboard/pages/admin-dashboard-main.tsx'; +import { RequireAuth } from '../../shared/require-auth.tsx'; +import { RequireAuthAdmin } from '../../shared/require-auth-admin.tsx'; export const HomeRoutes: React.FC = () => { - return ( - - }> - } /> - } /> - {/* Public route - user profiles are viewable without authentication */} - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - ); + return ( + + }> + } /> + } /> + {/* Public route - user profiles are viewable without authentication */} + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + ); }; From a7b4bce4452d64db4bc342a19f81e5a608e44f5b Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 6 Jan 2026 01:12:20 +0530 Subject: [PATCH 36/42] refactor: remove unused getUserDisplayName and use direct user displayName field --- .../messages/components/conversation-box.tsx | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx index 5ad640517..fe28371c3 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/messages/components/conversation-box.tsx @@ -7,22 +7,6 @@ interface ConversationBoxProps { data: Conversation; } -// Helper function to extract display name from User type -// This is a module-level pure function that can be called directly. -const getUserDisplayName = ( - user: Conversation['sharer'] | Conversation['reserver'] | undefined, -): string => { - if (!user) return 'Unknown'; - // Handle both PersonalUser and AdminUser account structures - const account = 'account' in user ? user.account : undefined; - const firstName = account?.profile?.firstName; - const lastName = account?.profile?.lastName; - if (firstName || lastName) { - return [firstName, lastName].filter(Boolean).join(' '); - } - return account?.username || 'Unknown'; -}; - export const ConversationBox: React.FC = (props) => { const [messageText, setMessageText] = useState(''); @@ -40,7 +24,7 @@ export const ConversationBox: React.FC = (props) => { const sharerInfo = useMemo( () => ({ id: props.data.sharer?.id || '', - displayName: getUserDisplayName(props.data.sharer), + displayName: props.data.sharer?.account?.profile?.firstName ?? 'Unknown', }), [props.data?.sharer], ); @@ -48,7 +32,8 @@ export const ConversationBox: React.FC = (props) => { const reserverInfo = useMemo( () => ({ id: props.data.reserver?.id || '', - displayName: getUserDisplayName(props.data.reserver), + displayName: + props.data.reserver?.account?.profile?.firstName ?? 'Unknown', }), [props.data?.reserver], ); From becccd92c4580734d0618783ba11ff70d8e5475c Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 6 Jan 2026 01:15:17 +0530 Subject: [PATCH 37/42] refactor: remove requestedById from ListingRequestData interface and sample data --- .../home/my-listings/components/my-listings-dashboard.types.ts | 1 - .../home/my-listings/components/requests-table.stories.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts index ef3a7bdfe..8aaadd577 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/my-listings-dashboard.types.ts @@ -13,7 +13,6 @@ export interface ListingRequestData { title: string; image?: string | null; requestedBy: string; - requestedById?: string | null; requestedOn: string; reservationPeriod: string; status: string; diff --git a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx index f3c0d6cc7..7a77b31bc 100644 --- a/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx +++ b/apps/ui-sharethrift/src/components/layouts/home/my-listings/components/requests-table.stories.tsx @@ -27,7 +27,6 @@ const sampleData: ListingRequestData[] = [ title: 'Cordless Drill', image: '/assets/item-images/placeholder.png', requestedBy: '@alice', - requestedById: '507f1f77bcf86cd799439011', requestedOn: '2025-01-01T00:00:00.000Z', reservationPeriod: '2025-01-05 - 2025-01-10', status: 'Pending', @@ -37,7 +36,6 @@ const sampleData: ListingRequestData[] = [ title: 'Camera', image: '/assets/item-images/placeholder.png', requestedBy: '@unknown', - requestedById: null, requestedOn: '2025-01-02T00:00:00.000Z', reservationPeriod: 'N/A', status: 'Accepted', From 91a69d9c028dd1cb0146a8579350130e0e75ec6e Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 6 Jan 2026 01:33:40 +0530 Subject: [PATCH 38/42] refactor: clean up reservation-request resolver responsibilities --- ...uery-listing-requests-by-sharer-id.feature | 4 +- .../reservation-request/index.ts | 6 +- ...uery-listing-requests-by-sharer-id.test.ts | 37 +- .../query-listing-requests-by-sharer-id.ts | 103 +++- .../reservation-request.resolvers.feature | 38 +- .../reservation-request.paginate.test.ts | 115 +---- .../reservation-request.resolvers.test.ts | 460 ++---------------- .../reservation-request.resolvers.ts | 128 +---- 8 files changed, 198 insertions(+), 693 deletions(-) diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/query-listing-requests-by-sharer-id.feature b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/query-listing-requests-by-sharer-id.feature index 36f6ccb6a..0f35fdd6f 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/query-listing-requests-by-sharer-id.feature +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/features/query-listing-requests-by-sharer-id.feature @@ -4,10 +4,10 @@ Feature: Query Listing Reservation Requests By Sharer ID Given a valid sharer ID "user-123" And the sharer has listings with 4 reservation requests When the queryListingRequestsBySharerId command is executed - Then 4 reservation requests should be returned + Then 4 listing requests should be returned in a page Scenario: Retrieving requests for sharer with no listings or requests Given a valid sharer ID "user-456" And the sharer has no reservation requests When the queryListingRequestsBySharerId command is executed - Then an empty array should be returned + Then an empty page should be returned diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/index.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/index.ts index e459f01a2..c125b4e7f 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/index.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/index.ts @@ -7,7 +7,7 @@ import { type ReservationRequestQueryByIdCommand, queryById } from './query-by-i import { type ReservationRequestCreateCommand, create } from './create.ts'; import { type ReservationRequestQueryOverlapByListingIdAndReservationPeriodCommand, queryOverlapByListingIdAndReservationPeriod } from './query-overlap-by-listing-id-and-reservation-period.ts'; import { type ReservationRequestQueryActiveByListingIdCommand, queryActiveByListingId } from './query-active-by-listing-id.ts'; -import { type ReservationRequestQueryListingRequestsBySharerIdCommand, queryListingRequestsBySharerId } from './query-listing-requests-by-sharer-id.ts'; +import { type ListingRequestPage, type ReservationRequestQueryListingRequestsBySharerIdCommand, queryListingRequestsBySharerId } from './query-listing-requests-by-sharer-id.ts'; export interface ReservationRequestApplicationService { queryById: (command: ReservationRequestQueryByIdCommand) => Promise, @@ -16,7 +16,7 @@ export interface ReservationRequestApplicationService { queryActiveByReserverIdAndListingId: (command: ReservationRequestQueryActiveByReserverIdAndListingIdCommand) => Promise, queryOverlapByListingIdAndReservationPeriod: (command: ReservationRequestQueryOverlapByListingIdAndReservationPeriodCommand) => Promise, queryActiveByListingId: (command: ReservationRequestQueryActiveByListingIdCommand) => Promise, - queryListingRequestsBySharerId: (command: ReservationRequestQueryListingRequestsBySharerIdCommand) => Promise, + queryListingRequestsBySharerId: (command: ReservationRequestQueryListingRequestsBySharerIdCommand) => Promise, create: (command: ReservationRequestCreateCommand) => Promise, } @@ -30,7 +30,7 @@ export const ReservationRequest = ( queryActiveByReserverIdAndListingId: queryActiveByReserverIdAndListingId(dataSources), queryOverlapByListingIdAndReservationPeriod: queryOverlapByListingIdAndReservationPeriod(dataSources), queryActiveByListingId: queryActiveByListingId(dataSources), - queryListingRequestsBySharerId: queryListingRequestsBySharerId(dataSources), + queryListingRequestsBySharerId: queryListingRequestsBySharerId(dataSources), create: create(dataSources), } } \ No newline at end of file diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.test.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.test.ts index e5b567ddf..9893eb1d6 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.test.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.test.ts @@ -37,7 +37,14 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { // biome-ignore lint/suspicious/noExplicitAny: Test mock type assertion } as any; - command = { sharerId: 'user-123' }; + command = { + sharerId: 'user-123', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'requestedOn', order: 'descend' }, + }; result = undefined; }); @@ -45,7 +52,14 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { "Successfully retrieving reservation requests for sharer's listings", ({ Given, And, When, Then }) => { Given('a valid sharer ID "user-123"', () => { - command = { sharerId: 'user-123' }; + command = { + sharerId: 'user-123', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'requestedOn', order: 'descend' }, + }; }); And('the sharer has listings with 4 reservation requests', () => { @@ -87,9 +101,10 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }, ); - Then('4 reservation requests should be returned', () => { + Then('4 listing requests should be returned in a page', () => { expect(result).toBeDefined(); - expect(result.length).toBe(4); + expect(result.items.length).toBe(4); + expect(result.total).toBe(4); }); }, ); @@ -98,7 +113,14 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { 'Retrieving requests for sharer with no listings or requests', ({ Given, And, When, Then }) => { Given('a valid sharer ID "user-456"', () => { - command = { sharerId: 'user-456' }; + command = { + sharerId: 'user-456', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'requestedOn', order: 'descend' }, + }; }); And('the sharer has no reservation requests', () => { @@ -118,9 +140,10 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }, ); - Then('an empty array should be returned', () => { + Then('an empty page should be returned', () => { expect(result).toBeDefined(); - expect(result.length).toBe(0); + expect(result.items.length).toBe(0); + expect(result.total).toBe(0); }); }, ); diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.ts index b4b4b06ff..c5052b88d 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.ts @@ -3,19 +3,112 @@ import type { DataSources } from '@sthrift/persistence'; export interface ReservationRequestQueryListingRequestsBySharerIdCommand { sharerId: string; + page: number; + pageSize: number; + searchText: string; + statusFilters: string[]; + sorter: { field: string; order: string }; fields?: string[]; } +export interface ListingRequestPageItem { + id: string; + title: string; + image?: string | null; + requestedBy: string; + requestedOn: string; + reservationPeriod: string; + status: string; +} + +export interface ListingRequestPage { + items: ListingRequestPageItem[]; + total: number; + page: number; + pageSize: number; +} + // Temporary implementation backed by mock data in persistence read repository export const queryListingRequestsBySharerId = ( dataSources: DataSources, ) => { return async ( command: ReservationRequestQueryListingRequestsBySharerIdCommand, - ): Promise => { - return await dataSources.readonlyDataSource.ReservationRequest.ReservationRequest.ReservationRequestReadRepo.getListingRequestsBySharerId( - command.sharerId, - { fields: command.fields } - ); + ): Promise => { + const requests: Domain.Contexts.ReservationRequest.ReservationRequest.ReservationRequestEntityReference[] = + await dataSources.readonlyDataSource.ReservationRequest.ReservationRequest.ReservationRequestReadRepo.getListingRequestsBySharerId( + command.sharerId, + { fields: command.fields }, + ); + + const mapped: ListingRequestPageItem[] = requests.map((r) => { + const start = + r.reservationPeriodStart instanceof Date + ? r.reservationPeriodStart + : undefined; + const end = + r.reservationPeriodEnd instanceof Date ? r.reservationPeriodEnd : undefined; + return { + id: r.id, + title: + r.listing && typeof r.listing === 'object' && 'title' in r.listing + ? ((r.listing as { title?: string }).title ?? 'Unknown') + : 'Unknown', + image: '/assets/item-images/placeholder.png', + requestedBy: + r.reserver?.account?.username != null + ? `@${r.reserver.account.username}` + : '@unknown', + requestedOn: + r.createdAt instanceof Date + ? r.createdAt.toISOString() + : new Date().toISOString(), + reservationPeriod: `${start ? start.toISOString().slice(0, 10) : 'N/A'} - ${end ? end.toISOString().slice(0, 10) : 'N/A'}`, + status: r.state ?? 'Pending', + }; + }); + + let working = mapped; + const searchText = command.searchText.trim(); + if (searchText.length > 0) { + const term = searchText.toLowerCase(); + working = working.filter((m) => m.title.toLowerCase().includes(term)); + } + + if (command.statusFilters.length > 0) { + working = working.filter((m) => command.statusFilters.includes(m.status)); + } + + const order = command.sorter.order; + if (command.sorter.field && (order === 'ascend' || order === 'descend')) { + const field = command.sorter.field as keyof ListingRequestPageItem; + working = [...working].sort((a, b) => { + const A = a[field]; + const B = b[field]; + if (A == null) { + return order === 'ascend' ? -1 : 1; + } + if (B == null) { + return order === 'ascend' ? 1 : -1; + } + if (A < B) { + return order === 'ascend' ? -1 : 1; + } + if (A > B) { + return order === 'ascend' ? 1 : -1; + } + return 0; + }); + } + + const total = working.length; + const startIndex = (command.page - 1) * command.pageSize; + const endIndex = startIndex + command.pageSize; + return { + items: working.slice(startIndex, endIndex), + total, + page: command.page, + pageSize: command.pageSize, + }; }; }; diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature b/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature index 68ddb228e..abc5914cc 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/features/reservation-request.resolvers.feature @@ -42,28 +42,9 @@ So that I can view my reservations and make new ones through the GraphQL API Given a valid sharerId And valid pagination arguments (page, pageSize) When the myListingsRequests query is executed - Then it should call ReservationRequest.queryListingRequestsBySharerId with the provided sharerId - And it should paginate and map the results using paginateAndFilterListingRequests + Then it should call ReservationRequest.queryListingRequestsBySharerId with the provided arguments And it should return items, total, page, and pageSize - Scenario: Filtering myListingsRequests by search text - Given reservation requests for a sharer - And a searchText "camera" - When the myListingsRequests query is executed - Then only listings whose titles include "camera" should be returned - - Scenario: Filtering myListingsRequests by status - Given reservation requests with mixed statuses ["Pending", "Approved"] - And a statusFilters ["Approved"] - When the myListingsRequests query is executed - Then only requests with status "Approved" should be included - - Scenario: Sorting myListingsRequests by requestedOn descending - Given reservation requests with varying createdAt timestamps - And sorter field "requestedOn" with order "descend" - When the myListingsRequests query is executed - Then results should be sorted by requestedOn in descending order - Scenario: Error while querying myListingsRequests Given ReservationRequest.queryListingRequestsBySharerId throws an error When the myListingsRequests query is executed @@ -125,20 +106,3 @@ So that I can view my reservations and make new ones through the GraphQL API And ReservationRequest.create throws an error When the createReservationRequest mutation is executed Then it should propagate the error message - - Scenario: Mapping listing request fields - Given a ListingRequestDomainShape object with title, state, and reserver username - When paginateAndFilterListingRequests is called - Then it should map title, requestedBy, requestedOn, reservationPeriod, and status into ListingRequestUiShape - And missing fields should default to 'Unknown', '@unknown', or 'Pending' as appropriate - - Scenario: Paginating listing requests - Given 25 listing requests and a pageSize of 10 - When paginateAndFilterListingRequests is called for page 2 - Then it should return 10 items starting from index 10 and total 25 - - Scenario: Sorting listing requests by title ascending - Given multiple listing requests with varying titles - And sorter field "title" with order "ascend" - When paginateAndFilterListingRequests is called - Then the results should be sorted alphabetically by title \ No newline at end of file diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts index 88f70e4de..faa23c155 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.paginate.test.ts @@ -1,111 +1,12 @@ -import { describe, it, expect } from 'vitest'; -import { paginateAndFilterListingRequests } from './reservation-request.resolvers.ts'; +import { describe, expect, it } from 'vitest'; +import * as reservationRequestModule from './reservation-request.resolvers.ts'; -const baseRequest = { - id: 'req-1', - createdAt: new Date('2025-01-01T00:00:00Z'), - reservationPeriodStart: new Date('2025-01-05T00:00:00Z'), - reservationPeriodEnd: new Date('2025-01-10T00:00:00Z'), - listing: { title: 'Cordless Drill' }, - reserver: { id: 'user-1', account: { username: 'alice' } }, -} as const; - -describe('paginateAndFilterListingRequests', () => { - it('maps domain shape to UI shape with defaults', () => { - const result = paginateAndFilterListingRequests([baseRequest], { - page: 1, - pageSize: 10, - statusFilters: [], - }); - - expect(result.items).toHaveLength(1); - const item = result.items[0]; - if (!item) { - throw new Error('Expected a single item in results'); - } - expect(item.title).toBe('Cordless Drill'); - expect(item.image).toBe('/assets/item-images/placeholder.png'); - expect(item.requestedBy).toBe('@alice'); - expect(item.requestedOn).toBe('2025-01-01T00:00:00.000Z'); - expect(item.reservationPeriod).toBe('2025-01-05 - 2025-01-10'); - expect(item.status).toBe('Pending'); - expect(item._raw.id).toBe('req-1'); - }); - - it('filters by searchText (case-insensitive)', () => { - const result = paginateAndFilterListingRequests( - [ - { ...baseRequest, id: 'a', listing: { title: 'Camera' } }, - { ...baseRequest, id: 'b', listing: { title: 'Drone' } }, - ], - { page: 1, pageSize: 10, statusFilters: [], searchText: 'camera' }, - ); - - expect(result.items.map((i) => i.id)).toEqual(['a']); - }); - - it('filters by statusFilters', () => { - const result = paginateAndFilterListingRequests( - [ - { ...baseRequest, id: 'a', state: 'Pending' }, - { ...baseRequest, id: 'b', state: 'Accepted' }, - ], - { page: 1, pageSize: 10, statusFilters: ['Accepted'] }, - ); - - expect(result.items.map((i) => i.id)).toEqual(['b']); - }); - - it('supports sorting with nulls and mixed values', () => { - const resultAsc = paginateAndFilterListingRequests( - [ - { ...baseRequest, id: 'a', reserver: { account: { username: 'alice' } } }, - { ...baseRequest, id: 'b', reserver: { id: 'user-2', account: { username: 'bob' } } }, - { ...baseRequest, id: 'c', reserver: { id: 'user-1', account: { username: 'cara' } } }, - ], - { - page: 1, - pageSize: 10, - statusFilters: [], - sorter: { field: 'requestedById', order: 'ascend' }, - }, - ); - - expect(resultAsc.items.map((i) => i.id)).toEqual(['a', 'c', 'b']); - - const resultDesc = paginateAndFilterListingRequests( - [ - { ...baseRequest, id: 'a', reserver: { account: { username: 'alice' } } }, - { ...baseRequest, id: 'b', reserver: { id: 'user-2', account: { username: 'bob' } } }, - { ...baseRequest, id: 'c', reserver: { id: 'user-1', account: { username: 'cara' } } }, +describe('reservation-request.resolvers exports', () => { + it('does not export paginateAndFilterListingRequests', () => { + expect( + (reservationRequestModule as unknown as Record)[ + 'paginateAndFilterListingRequests' ], - { - page: 1, - pageSize: 10, - statusFilters: [], - sorter: { field: 'requestedById', order: 'descend' }, - }, - ); - - expect(resultDesc.items.map((i) => i.id)).toEqual(['b', 'c', 'a']); - }); - - it('paginates results correctly', () => { - const requests = Array.from({ length: 25 }).map((_, index) => ({ - ...baseRequest, - id: `req-${index + 1}`, - })); - - const page2 = paginateAndFilterListingRequests(requests, { - page: 2, - pageSize: 10, - statusFilters: [], - }); - - expect(page2.items).toHaveLength(10); - expect(page2.items[0]?.id).toBe('req-11'); - expect(page2.total).toBe(25); - expect(page2.page).toBe(2); - expect(page2.pageSize).toBe(10); + ).toBeUndefined(); }); }); diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts index 4a20f5c4f..25c16449e 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.test.ts @@ -281,19 +281,22 @@ test.for(feature, ({ Scenario }) => { context = makeMockGraphContext(); }); And('valid pagination arguments (page, pageSize)', () => { - const mockRequests = [ - createMockReservationRequest({ - id: '1', - state: 'Requested', - createdAt: new Date('2024-01-01'), - reservationPeriodStart: new Date('2024-02-01'), - reservationPeriodEnd: new Date('2024-02-10'), - listing: { title: 'Test Item' } as ItemListingEntity, - reserver: { - account: { username: 'testuser' }, - } as PersonalUserEntity, - }), - ]; + const mockRequests = { + items: [ + { + id: '1', + title: 'Test Item', + image: '/assets/item-images/placeholder.png', + requestedBy: '@testuser', + requestedOn: new Date('2024-01-01').toISOString(), + reservationPeriod: '2024-02-01 - 2024-02-10', + status: 'Requested', + }, + ], + total: 1, + page: 1, + pageSize: 10, + }; vi.mocked( context.applicationServices.ReservationRequest.ReservationRequest .queryListingRequestsBySharerId, @@ -305,27 +308,38 @@ test.for(feature, ({ Scenario }) => { sharerId: string; page: number; pageSize: number; + searchText: string; + statusFilters: string[]; + sorter: { field: string; order: string }; }>; result = await resolver( {}, - { sharerId, page: 1, pageSize: 10 }, + { + sharerId, + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'requestedOn', order: 'descend' }, + }, context, {} as never, ); }); Then( - 'it should call ReservationRequest.queryListingRequestsBySharerId with the provided sharerId', + 'it should call ReservationRequest.queryListingRequestsBySharerId with the provided arguments', () => { expect( context.applicationServices.ReservationRequest.ReservationRequest .queryListingRequestsBySharerId, - ).toHaveBeenCalledWith({ sharerId }); - }, - ); - And( - 'it should paginate and map the results using paginateAndFilterListingRequests', - () => { - expect(result).toBeDefined(); + ).toHaveBeenCalledWith({ + sharerId, + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'requestedOn', order: 'descend' }, + }); }, ); And('it should return items, total, page, and pageSize', () => { @@ -337,130 +351,6 @@ test.for(feature, ({ Scenario }) => { }, ); - Scenario( - 'Filtering myListingsRequests by search text', - ({ Given, And, When, Then }) => { - Given('reservation requests for a sharer', () => { - context = makeMockGraphContext(); - const mockRequests = [ - createMockReservationRequest({ - id: '1', - state: 'Requested', - createdAt: new Date(), - listing: { title: 'Camera' } as ItemListingEntity, - reserver: { account: { username: 'user1' } } as PersonalUserEntity, - }), - createMockReservationRequest({ - id: '2', - state: 'Requested', - createdAt: new Date(), - listing: { title: 'Drone' } as ItemListingEntity, - reserver: { account: { username: 'user2' } } as PersonalUserEntity, - }), - ]; - vi.mocked( - context.applicationServices.ReservationRequest.ReservationRequest - .queryListingRequestsBySharerId, - ).mockResolvedValue(mockRequests); - }); - And('a searchText "camera"', () => { - // Searchtext will be used in the When step - }); - When('the myListingsRequests query is executed', async () => { - const resolver = reservationRequestResolvers.Query - ?.myListingsRequests as TestResolver<{ - sharerId: string; - page: number; - pageSize: number; - searchText: string; - }>; - result = await resolver( - {}, - { - sharerId: 'sharer-123', - page: 1, - pageSize: 10, - searchText: 'camera', - }, - context, - {} as never, - ); - }); - Then( - 'only listings whose titles include "camera" should be returned', - () => { - const items = (result as { items: { title: string }[] }).items; - expect(items).toHaveLength(1); - expect(items[0]?.title).toBe('Camera'); - }, - ); - }, - ); - - Scenario( - 'Filtering myListingsRequests by status', - ({ Given, And, When, Then }) => { - Given( - 'reservation requests with mixed statuses ["Pending", "Approved"]', - () => { - context = makeMockGraphContext(); - const mockRequests = [ - createMockReservationRequest({ - id: '1', - state: 'Accepted', - createdAt: new Date(), - listing: { title: 'Item 1' } as ItemListingEntity, - reserver: { - account: { username: 'user1' }, - } as PersonalUserEntity, - }), - createMockReservationRequest({ - id: '2', - state: 'Requested', - createdAt: new Date(), - listing: { title: 'Item 2' } as ItemListingEntity, - reserver: { - account: { username: 'user2' }, - } as PersonalUserEntity, - }), - ]; - vi.mocked( - context.applicationServices.ReservationRequest.ReservationRequest - .queryListingRequestsBySharerId, - ).mockResolvedValue(mockRequests); - }, - ); - And('a statusFilters ["Approved"]', () => { - // Status filters will be used in the When step - }); - When('the myListingsRequests query is executed', async () => { - const resolver = reservationRequestResolvers.Query - ?.myListingsRequests as unknown as TestResolver<{ - sharerId: string; - page: number; - pageSize: number; - statusFilters: string[]; - }>; - result = await resolver( - {}, - { - sharerId: 'sharer-123', - page: 1, - pageSize: 10, - statusFilters: ['Accepted'], - }, - context, - {} as never, - ); - }); - Then('only requests with status "Approved" should be included', () => { - const items = (result as { items: { status: string }[] }).items; - expect(items).toHaveLength(1); - expect(items[0]?.status).toBe('Accepted'); - }); - }, - ); - Scenario( 'Querying active reservation for a specific listing', ({ Given, When, Then, And }) => { @@ -806,74 +696,6 @@ test.for(feature, ({ Scenario }) => { }, ); - Scenario( - 'Sorting myListingsRequests by requestedOn descending', - ({ Given, And, When, Then }) => { - Given('reservation requests with varying createdAt timestamps', () => { - context = makeMockGraphContext(); - const mockRequests = [ - createMockReservationRequest({ - id: '1', - state: 'Requested', - createdAt: new Date('2024-01-01'), - listing: { title: 'Item 1' } as ItemListingEntity, - reserver: { account: { username: 'user1' } } as PersonalUserEntity, - }), - createMockReservationRequest({ - id: '2', - state: 'Requested', - createdAt: new Date('2024-01-03'), - listing: { title: 'Item 2' } as ItemListingEntity, - reserver: { account: { username: 'user2' } } as PersonalUserEntity, - }), - createMockReservationRequest({ - id: '3', - state: 'Requested', - createdAt: new Date('2024-01-02'), - listing: { title: 'Item 3' } as ItemListingEntity, - reserver: { account: { username: 'user3' } } as PersonalUserEntity, - }), - ]; - vi.mocked( - context.applicationServices.ReservationRequest.ReservationRequest - .queryListingRequestsBySharerId, - ).mockResolvedValue(mockRequests); - }); - And('sorter field "requestedOn" with order "descend"', () => { - // Sorter will be used in the When step - }); - When('the myListingsRequests query is executed', async () => { - const resolver = reservationRequestResolvers.Query - ?.myListingsRequests as TestResolver<{ - sharerId: string; - page: number; - pageSize: number; - sorter?: { field: string; order: 'ascend' | 'descend' }; - }>; - result = await resolver( - {}, - { - sharerId: 'sharer-123', - page: 1, - pageSize: 10, - sorter: { field: 'requestedOn', order: 'descend' }, - }, - context, - {} as never, - ); - }); - Then( - 'results should be sorted by requestedOn in descending order', - () => { - const items = (result as { items: { requestedOn: string }[] }).items; - expect(items.length).toBeGreaterThan(0); - // Just verify that sorting was applied (items are in expected order based on input) - // The actual sorting logic is tested by the implementation - expect(items.length).toBe(3); - }, - ); - }, - ); Scenario( 'Error while querying myListingsRequests', @@ -895,10 +717,20 @@ test.for(feature, ({ Scenario }) => { sharerId: string; page: number; pageSize: number; + searchText: string; + statusFilters: string[]; + sorter: { field: string; order: string }; }>; await resolver( {}, - { sharerId: 'sharer-123', page: 1, pageSize: 10 }, + { + sharerId: 'sharer-123', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'requestedOn', order: 'descend' }, + }, context, {} as never, ); @@ -955,202 +787,4 @@ test.for(feature, ({ Scenario }) => { }, ); - Scenario('Mapping listing request fields', ({ Given, When, Then, And }) => { - // This is tested implicitly through other scenarios that use myListingsRequests - // as they all verify the mapping occurs correctly - Given( - 'a ListingRequestDomainShape object with title, state, and reserver username', - () => { - context = makeMockGraphContext(); - const mockRequests = [ - createMockReservationRequest({ - id: '1', - state: 'Requested', - createdAt: new Date('2024-01-01'), - reservationPeriodStart: new Date('2024-02-01'), - reservationPeriodEnd: new Date('2024-02-10'), - listing: { title: 'Test Item' } as ItemListingEntity, - reserver: { - account: { username: 'testuser' }, - } as PersonalUserEntity, - }), - ]; - vi.mocked( - context.applicationServices.ReservationRequest.ReservationRequest - .queryListingRequestsBySharerId, - ).mockResolvedValue(mockRequests); - }, - ); - When('paginateAndFilterListingRequests is called', async () => { - const resolver = reservationRequestResolvers.Query - ?.myListingsRequests as TestResolver<{ - sharerId: string; - page: number; - pageSize: number; - }>; - result = await resolver( - {}, - { sharerId: 'sharer-123', page: 1, pageSize: 10 }, - context, - {} as never, - ); - }); - Then( - 'it should map title, requestedBy, requestedOn, reservationPeriod, and status into ListingRequestUiShape', - () => { - const items = ( - result as { - items: { - title: string; - requestedBy: string; - requestedOn: string; - reservationPeriod: string; - status: string; - }[]; - } - ).items; - expect(items[0]).toHaveProperty('title'); - expect(items[0]).toHaveProperty('requestedBy'); - expect(items[0]).toHaveProperty('requestedOn'); - expect(items[0]).toHaveProperty('reservationPeriod'); - expect(items[0]).toHaveProperty('status'); - }, - ); - And( - "missing fields should default to 'Unknown', '@unknown', or 'Pending' as appropriate", - () => { - // Test with missing fields - const items = ( - result as { - items: { - title: string; - requestedBy: string; - status: string; - }[]; - } - ).items; - expect(items[0]?.title).toBe('Test Item'); - expect(items[0]?.requestedBy).toBe('@testuser'); - expect(items[0]?.status).toBe('Requested'); - }, - ); - }); - - Scenario('Paginating listing requests', ({ Given, When, Then }) => { - Given('25 listing requests and a pageSize of 10', () => { - context = makeMockGraphContext(); - const mockRequests = Array.from({ length: 25 }, (_, i) => - createMockReservationRequest({ - id: `${i + 1}`, - state: 'Requested', - createdAt: new Date(), - listing: { title: `Item ${i + 1}` } as ItemListingEntity, - reserver: { - account: { username: `user${i + 1}` }, - } as PersonalUserEntity, - }), - ); - vi.mocked( - context.applicationServices.ReservationRequest.ReservationRequest - .queryListingRequestsBySharerId, - ).mockResolvedValue(mockRequests); - }); - When('paginateAndFilterListingRequests is called for page 2', async () => { - const resolver = reservationRequestResolvers.Query - ?.myListingsRequests as TestResolver<{ - sharerId: string; - page: number; - pageSize: number; - }>; - result = await resolver( - {}, - { sharerId: 'sharer-123', page: 2, pageSize: 10 }, - context, - {} as never, - ); - }); - Then( - 'it should return 10 items starting from index 10 and total 25', - () => { - const paginatedResult = result as { - items: unknown[]; - total: number; - page: number; - pageSize: number; - }; - expect(paginatedResult.items.length).toBe(10); - expect(paginatedResult.total).toBe(25); - expect(paginatedResult.page).toBe(2); - expect(paginatedResult.pageSize).toBe(10); - }, - ); - }); - - Scenario( - 'Sorting listing requests by title ascending', - ({ Given, And, When, Then }) => { - Given('multiple listing requests with varying titles', () => { - context = makeMockGraphContext(); - const mockRequests = [ - createMockReservationRequest({ - id: '1', - state: 'Requested', - createdAt: new Date(), - listing: { title: 'Zebra Camera' } as ItemListingEntity, - reserver: { account: { username: 'user1' } } as PersonalUserEntity, - }), - createMockReservationRequest({ - id: '2', - state: 'Requested', - createdAt: new Date(), - listing: { title: 'Apple Drone' } as ItemListingEntity, - reserver: { account: { username: 'user2' } } as PersonalUserEntity, - }), - createMockReservationRequest({ - id: '3', - state: 'Requested', - createdAt: new Date(), - listing: { title: 'Microphone Beta' } as ItemListingEntity, - reserver: { account: { username: 'user3' } } as PersonalUserEntity, - }), - ]; - vi.mocked( - context.applicationServices.ReservationRequest.ReservationRequest - .queryListingRequestsBySharerId, - ).mockResolvedValue(mockRequests); - }); - And('sorter field "title" with order "ascend"', () => { - // Sorter will be used in the When step - }); - When('paginateAndFilterListingRequests is called', async () => { - const resolver = reservationRequestResolvers.Query - ?.myListingsRequests as TestResolver<{ - sharerId: string; - page: number; - pageSize: number; - sorter?: { field: string; order: 'ascend' | 'descend' }; - }>; - result = await resolver( - {}, - { - sharerId: 'sharer-123', - page: 1, - pageSize: 10, - sorter: { field: 'title', order: 'ascend' }, - }, - context, - {} as never, - ); - }); - Then('the results should be sorted alphabetically by title', () => { - const items = (result as { items: { title: string }[] }).items; - expect(items.length).toBe(3); - // Just verify that the sorting was applied and items are present - const titles = items.map((item) => item.title); - expect(titles).toContain('Apple Drone'); - expect(titles).toContain('Zebra Camera'); - expect(titles).toContain('Microphone Beta'); - }); - }, - ); }); diff --git a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts index 28ab4d15e..9707f9c01 100644 --- a/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts +++ b/packages/sthrift/graphql/src/schema/types/reservation-request/reservation-request.resolvers.ts @@ -1,119 +1,12 @@ import type { GraphContext } from '../../../init/context.ts'; import type { GraphQLResolveInfo } from 'graphql'; import type { Resolvers } from '../../builder/generated.ts'; +import type { QueryMyListingsRequestsArgs } from '../../builder/generated.ts'; import { PopulateItemListingFromField, PopulateUserFromField, } from '../../resolver-helper.ts'; -interface ListingRequestDomainShape { - id: string; - state?: string; - createdAt?: Date; - reservationPeriodStart?: Date; - reservationPeriodEnd?: Date; - listing?: { title?: string; [k: string]: unknown }; - reserver?: { id?: string; account?: { username?: string } }; - [k: string]: unknown; // allow passthrough -} - -interface ListingRequestUiShape { - id: string; - title: string; - image: string; - requestedBy: string; - requestedById: string | null; - requestedOn: string; - reservationPeriod: string; - status: string; - _raw: ListingRequestDomainShape; - [k: string]: unknown; // enable dynamic field sorting access -} - -// Exported for unit testing the request mapping/pagination logic -export function paginateAndFilterListingRequests( - requests: ListingRequestDomainShape[], - options: { - page: number; - pageSize: number; - searchText?: string; - statusFilters: string[]; - sorter?: { field: string | null; order: 'ascend' | 'descend' | null }; - }, -) { - const filtered = [...requests]; - - // Map domain objects into shape expected by client (flatten minimal fields) - const mapped: ListingRequestUiShape[] = filtered.map((r) => { - const start = - r.reservationPeriodStart instanceof Date - ? r.reservationPeriodStart - : undefined; - const end = - r.reservationPeriodEnd instanceof Date - ? r.reservationPeriodEnd - : undefined; - return { - id: r.id, - title: r.listing?.title ?? 'Unknown', - image: '/assets/item-images/placeholder.png', // TODO: map real image when available - requestedBy: r.reserver?.account?.username - ? `@${r.reserver.account.username}` - : '@unknown', - requestedById: r.reserver?.id ?? null, - requestedOn: - r.createdAt instanceof Date - ? r.createdAt.toISOString() - : new Date().toISOString(), - reservationPeriod: `${start ? start.toISOString().slice(0, 10) : 'N/A'} - ${end ? end.toISOString().slice(0, 10) : 'N/A'}`, - status: r.state ?? 'Pending', - _raw: r, - }; - }); - - let working = mapped; - if (options.searchText) { - const term = options.searchText.toLowerCase(); - working = working.filter((m) => m.title.toLowerCase().includes(term)); - } - - if (options.statusFilters?.length) { - working = working.filter((m) => options.statusFilters?.includes(m.status)); - } - - if (options.sorter?.field) { - const { field, order } = options.sorter; - working.sort((a: ListingRequestUiShape, b: ListingRequestUiShape) => { - const sortField = field as keyof ListingRequestUiShape; - const A = a[sortField]; - const B = b[sortField]; - if (A == null) { - return order === 'ascend' ? -1 : 1; - } - if (B == null) { - return order === 'ascend' ? 1 : -1; - } - if (A < B) { - return order === 'ascend' ? -1 : 1; - } - if (A > B) { - return order === 'ascend' ? 1 : -1; - } - return 0; - }); - } - - const total = working.length; - const startIndex = (options.page - 1) * options.pageSize; - const endIndex = startIndex + options.pageSize; - return { - items: working.slice(startIndex, endIndex), - total, - page: options.page, - pageSize: options.pageSize, - }; -} - const reservationRequest: Resolvers = { ReservationRequest: { reserver: PopulateUserFromField('reserver'), @@ -146,23 +39,20 @@ const reservationRequest: Resolvers = { }, myListingsRequests: async ( _parent: unknown, - args, + args: QueryMyListingsRequestsArgs, context: GraphContext, ) => { - // Fetch reservation requests for listings owned by sharer from application services - const requests = - await context.applicationServices.ReservationRequest.ReservationRequest.queryListingRequestsBySharerId( - { - sharerId: args.sharerId, - }, - ); - return paginateAndFilterListingRequests( - requests as unknown as ListingRequestDomainShape[], + return await context.applicationServices.ReservationRequest.ReservationRequest.queryListingRequestsBySharerId( { + sharerId: args.sharerId, page: args.page, pageSize: args.pageSize, searchText: args.searchText, - statusFilters: [...(args.statusFilters ?? [])], + statusFilters: [...args.statusFilters], + sorter: { + field: args.sorter.field, + order: args.sorter.order, + }, }, ); }, From a090d9ce9f2c33d64fa9ebb15a89734e31c65001 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 6 Jan 2026 01:43:33 +0530 Subject: [PATCH 39/42] chore: override qs to 6.14.1 for audit --- package.json | 5 +++++ pnpm-lock.yaml | 36 +++++++++++++++++------------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 008df7185..5e50cdc3b 100644 --- a/package.json +++ b/package.json @@ -74,5 +74,10 @@ "typescript": "^5.8.3", "vite": "^7.0.4", "vitest": "^3.2.4" + }, + "pnpm": { + "overrides": { + "qs": "6.14.1" + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index deb2abe97..c7c6aa10e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,7 @@ catalogs: version: 9.1.17 overrides: - node-forge@<1.3.2: '>=1.3.2' + qs: 6.14.1 importers: @@ -8653,6 +8653,10 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + node-forge@1.3.2: resolution: {integrity: sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==} engines: {node: '>= 6.13.0'} @@ -9488,12 +9492,8 @@ packages: resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} engines: {node: '>=12.20'} - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} querystringify@2.2.0: @@ -15972,7 +15972,7 @@ snapshots: commander: 13.1.0 fs-extra: 11.3.2 hpagent: 1.2.0 - node-forge: 1.3.2 + node-forge: 1.3.1 properties-file: 3.6.1 proxy-from-env: 1.1.0 semver: 7.7.2 @@ -17363,7 +17363,7 @@ snapshots: http-errors: 2.0.0 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.13.0 + qs: 6.14.1 raw-body: 2.5.2 type-is: 1.6.18 unpipe: 1.0.0 @@ -17378,7 +17378,7 @@ snapshots: http-errors: 2.0.0 iconv-lite: 0.7.0 on-finished: 2.4.1 - qs: 6.14.0 + qs: 6.14.1 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -18730,7 +18730,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.14.1 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 @@ -18766,7 +18766,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.14.1 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.0 @@ -21185,6 +21185,8 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-forge@1.3.1: {} + node-forge@1.3.2: {} node-int64@0.4.0: {} @@ -22094,11 +22096,7 @@ snapshots: dependencies: escape-goat: 4.0.0 - qs@6.13.0: - dependencies: - side-channel: 1.1.0 - - qs@6.14.0: + qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -23519,7 +23517,7 @@ snapshots: formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.14.0 + qs: 6.14.1 transitivePeerDependencies: - supports-color @@ -23858,7 +23856,7 @@ snapshots: dayjs: 1.11.18 https-proxy-agent: 5.0.1 jsonwebtoken: 9.0.3 - qs: 6.14.0 + qs: 6.14.1 scmp: 2.1.0 xmlbuilder: 13.0.2 transitivePeerDependencies: From a1acd49005893041f5e2ccd3f562a610012a5ced Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 6 Jan 2026 02:30:26 +0530 Subject: [PATCH 40/42] refactor: simplify ListingRequestPageItem definition and usage --- .../query-listing-requests-by-sharer-id.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.ts index c5052b88d..e3cd8f670 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.ts @@ -11,23 +11,23 @@ export interface ReservationRequestQueryListingRequestsBySharerIdCommand { fields?: string[]; } -export interface ListingRequestPageItem { - id: string; - title: string; - image?: string | null; - requestedBy: string; - requestedOn: string; - reservationPeriod: string; - status: string; -} - export interface ListingRequestPage { - items: ListingRequestPageItem[]; + items: Array<{ + id: string; + title: string; + image?: string | null; + requestedBy: string; + requestedOn: string; + reservationPeriod: string; + status: string; + }>; total: number; page: number; pageSize: number; } +type ListingRequestPageItem = ListingRequestPage['items'][number]; + // Temporary implementation backed by mock data in persistence read repository export const queryListingRequestsBySharerId = ( dataSources: DataSources, @@ -41,7 +41,7 @@ export const queryListingRequestsBySharerId = ( { fields: command.fields }, ); - const mapped: ListingRequestPageItem[] = requests.map((r) => { + const mapped: ListingRequestPage['items'] = requests.map((r) => { const start = r.reservationPeriodStart instanceof Date ? r.reservationPeriodStart From 784983be8d57157deedc5f4703389c4a719a0b39 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Tue, 6 Jan 2026 10:59:58 +0530 Subject: [PATCH 41/42] test: add unit tests for queryListingRequestsBySharerId functionality --- ...uery-listing-requests-by-sharer-id.test.ts | 208 +++++++++++++++++- 1 file changed, 207 insertions(+), 1 deletion(-) diff --git a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.test.ts b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.test.ts index 9893eb1d6..3ca501186 100644 --- a/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.test.ts +++ b/packages/sthrift/application-services/src/contexts/reservation-request/reservation-request/query-listing-requests-by-sharer-id.test.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { describeFeature, loadFeature } from '@amiceli/vitest-cucumber'; import type { DataSources } from '@sthrift/persistence'; -import { expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { queryListingRequestsBySharerId, type ReservationRequestQueryListingRequestsBySharerIdCommand, @@ -148,3 +148,209 @@ test.for(feature, ({ Scenario, BeforeEachScenario }) => { }, ); }); + +describe('queryListingRequestsBySharerId (unit)', () => { + function makeMockDataSources(requests: unknown[]): DataSources { + return { + readonlyDataSource: { + ReservationRequest: { + ReservationRequest: { + ReservationRequestReadRepo: { + getListingRequestsBySharerId: vi + .fn() + .mockResolvedValue(requests), + }, + }, + }, + }, + // biome-ignore lint/suspicious/noExplicitAny: Test mock type assertion + } as any; + } + + it('filters by trimmed, case-insensitive searchText', async () => { + const mockDataSources = makeMockDataSources([ + { id: 'req-1', state: 'Requested', listing: { title: 'Red Bike' } }, + { id: 'req-2', state: 'Requested', listing: { title: 'Blue Car' } }, + ]); + + const command: ReservationRequestQueryListingRequestsBySharerIdCommand = { + sharerId: 'user-123', + page: 1, + pageSize: 10, + searchText: ' BIKE ', + statusFilters: [], + sorter: { field: 'requestedOn', order: 'descend' }, + }; + + const queryFn = queryListingRequestsBySharerId(mockDataSources); + const result = await queryFn(command); + expect(result.total).toBe(1); + expect(result.items[0]?.title).toBe('Red Bike'); + }); + + it('filters by statusFilters when provided', async () => { + const mockDataSources = makeMockDataSources([ + { id: 'req-1', state: 'Accepted', listing: { title: 'Item A' } }, + { id: 'req-2', state: 'Requested', listing: { title: 'Item B' } }, + ]); + + const command: ReservationRequestQueryListingRequestsBySharerIdCommand = { + sharerId: 'user-123', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: ['Accepted'], + sorter: { field: 'requestedOn', order: 'descend' }, + }; + + const queryFn = queryListingRequestsBySharerId(mockDataSources); + const result = await queryFn(command); + expect(result.total).toBe(1); + expect(result.items[0]?.status).toBe('Accepted'); + }); + + it('maps fallback values when listing/reserver/date fields are missing', async () => { + const mockDataSources = makeMockDataSources([ + { + id: 'req-1', + state: undefined, + listing: 'not-an-object', + reserver: { account: { username: null } }, + createdAt: 'not-a-date', + reservationPeriodStart: 'not-a-date', + reservationPeriodEnd: null, + }, + ]); + + const command: ReservationRequestQueryListingRequestsBySharerIdCommand = { + sharerId: 'user-123', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'requestedOn', order: 'descend' }, + }; + + const queryFn = queryListingRequestsBySharerId(mockDataSources); + const result = await queryFn(command); + expect(result.items[0]?.title).toBe('Unknown'); + expect(result.items[0]?.requestedBy).toBe('@unknown'); + expect(result.items[0]?.status).toBe('Pending'); + expect(result.items[0]?.reservationPeriod).toContain('N/A'); + expect(result.items[0]?.requestedOn).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:/, + ); + }); + + it('maps username, dates, and listing title when provided', async () => { + const mockDataSources = makeMockDataSources([ + { + id: 'req-1', + state: 'Requested', + listing: { title: 'Cordless Drill' }, + reserver: { account: { username: 'bob' } }, + createdAt: new Date('2025-01-01T00:00:00.000Z'), + reservationPeriodStart: new Date('2025-02-01T00:00:00.000Z'), + reservationPeriodEnd: new Date('2025-02-03T00:00:00.000Z'), + }, + ]); + + const command: ReservationRequestQueryListingRequestsBySharerIdCommand = { + sharerId: 'user-123', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'requestedOn', order: 'descend' }, + }; + + const queryFn = queryListingRequestsBySharerId(mockDataSources); + const result = await queryFn(command); + expect(result.items[0]?.title).toBe('Cordless Drill'); + expect(result.items[0]?.requestedBy).toBe('@bob'); + expect(result.items[0]?.requestedOn).toBe('2025-01-01T00:00:00.000Z'); + expect(result.items[0]?.reservationPeriod).toBe('2025-02-01 - 2025-02-03'); + }); + + it('falls back to "Unknown" when listing has a title field but it is undefined', async () => { + const mockDataSources = makeMockDataSources([ + { id: 'req-1', state: 'Requested', listing: { title: undefined } }, + ]); + + const command: ReservationRequestQueryListingRequestsBySharerIdCommand = { + sharerId: 'user-123', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'requestedOn', order: 'descend' }, + }; + + const queryFn = queryListingRequestsBySharerId(mockDataSources); + const result = await queryFn(command); + expect(result.items[0]?.title).toBe('Unknown'); + }); + + it('does not throw when sorting by an unknown field (null/undefined compare path)', async () => { + const mockDataSources = makeMockDataSources([ + { id: 'req-1', state: 'Requested', listing: { title: 'Item A' } }, + { id: 'req-2', state: 'Requested', listing: { title: 'Item B' } }, + ]); + + const command: ReservationRequestQueryListingRequestsBySharerIdCommand = { + sharerId: 'user-123', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'doesNotExist', order: 'ascend' }, + }; + + const queryFn = queryListingRequestsBySharerId(mockDataSources); + const result = await queryFn(command); + expect(result.total).toBe(2); + expect(result.items).toHaveLength(2); + }); + + it('sorts by title ascending', async () => { + const mockDataSources = makeMockDataSources([ + { id: 'req-1', state: 'Requested', listing: { title: 'C' } }, + { id: 'req-2', state: 'Requested', listing: { title: 'A' } }, + { id: 'req-3', state: 'Requested', listing: { title: 'B' } }, + ]); + + const command: ReservationRequestQueryListingRequestsBySharerIdCommand = { + sharerId: 'user-123', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'title', order: 'ascend' }, + }; + + const queryFn = queryListingRequestsBySharerId(mockDataSources); + const result = await queryFn(command); + expect(result.items.map((i) => i.title)).toEqual(['A', 'B', 'C']); + }); + + it('sorts by title descending', async () => { + const mockDataSources = makeMockDataSources([ + { id: 'req-1', state: 'Requested', listing: { title: 'A' } }, + { id: 'req-2', state: 'Requested', listing: { title: 'C' } }, + { id: 'req-3', state: 'Requested', listing: { title: 'B' } }, + ]); + + const command: ReservationRequestQueryListingRequestsBySharerIdCommand = { + sharerId: 'user-123', + page: 1, + pageSize: 10, + searchText: '', + statusFilters: [], + sorter: { field: 'title', order: 'descend' }, + }; + + const queryFn = queryListingRequestsBySharerId(mockDataSources); + const result = await queryFn(command); + expect(result.items.map((i) => i.title)).toEqual(['C', 'B', 'A']); + }); +}); From 318813fa8d24d32ee58a92dd03fda5d38a7968f1 Mon Sep 17 00:00:00 2001 From: vaibhavdani007 Date: Thu, 8 Jan 2026 01:02:13 +0530 Subject: [PATCH 42/42] feat(env): define ProcessEnv interface for environment variables refactor: simplify access to environment variables in the code test: update mock data source access in admin and personal user tests --- packages/cellix/mock-oauth2-server/src/env.d.ts | 15 +++++++++++++++ packages/cellix/mock-oauth2-server/src/index.ts | 15 +++++++-------- .../messaging-conversation.domain-adapter.ts | 2 +- .../admin-user/admin-user.read-repository.test.ts | 2 +- .../personal-user.read-repository.test.ts | 2 +- 5 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 packages/cellix/mock-oauth2-server/src/env.d.ts diff --git a/packages/cellix/mock-oauth2-server/src/env.d.ts b/packages/cellix/mock-oauth2-server/src/env.d.ts new file mode 100644 index 000000000..10c09a5e2 --- /dev/null +++ b/packages/cellix/mock-oauth2-server/src/env.d.ts @@ -0,0 +1,15 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + ALLOWED_REDIRECT_URI?: string; + Email?: string; + Admin_Email?: string; + Given_Name?: string; + Admin_Given_Name?: string; + Family_Name?: string; + Admin_Family_Name?: string; + } + } +} + +export {}; diff --git a/packages/cellix/mock-oauth2-server/src/index.ts b/packages/cellix/mock-oauth2-server/src/index.ts index 61f34b830..ce055ce1c 100644 --- a/packages/cellix/mock-oauth2-server/src/index.ts +++ b/packages/cellix/mock-oauth2-server/src/index.ts @@ -33,8 +33,7 @@ const redirectUriToAudience = new Map([ ]); // Deprecated: kept for backwards compatibility const allowedRedirectUri = - // biome-ignore lint:useLiteralKeys - process.env['ALLOWED_REDIRECT_URI'] || + process.env.ALLOWED_REDIRECT_URI || 'http://localhost:3000/auth-redirect-user'; // Type for user profile used in token claims interface TokenProfile { @@ -190,14 +189,14 @@ async function main() { // Use different credentials based on portal type const email = isAdminPortal - ? process.env['Admin_Email'] || process.env['Email'] || '' - : process.env['Email'] || ''; + ? process.env.Admin_Email || process.env.Email || '' + : process.env.Email || ''; const given_name = isAdminPortal - ? process.env['Admin_Given_Name'] || process.env['Given_Name'] || '' - : process.env['Given_Name'] || ''; + ? process.env.Admin_Given_Name || process.env.Given_Name || '' + : process.env.Given_Name || ''; const family_name = isAdminPortal - ? process.env['Admin_Family_Name'] || process.env['Family_Name'] || '' - : process.env['Family_Name'] || ''; + ? process.env.Admin_Family_Name || process.env.Family_Name || '' + : process.env.Family_Name || ''; const profile: TokenProfile = { aud: aud, // Now using proper audience identifier diff --git a/packages/sthrift/persistence/src/datasources/messaging/conversation/conversation/messaging-conversation.domain-adapter.ts b/packages/sthrift/persistence/src/datasources/messaging/conversation/conversation/messaging-conversation.domain-adapter.ts index 9c50e9efb..8697369d0 100644 --- a/packages/sthrift/persistence/src/datasources/messaging/conversation/conversation/messaging-conversation.domain-adapter.ts +++ b/packages/sthrift/persistence/src/datasources/messaging/conversation/conversation/messaging-conversation.domain-adapter.ts @@ -5,7 +5,7 @@ export function toDomainMessage( messagingMessage: MessageInstance, authorId: Domain.Contexts.Conversation.Conversation.AuthorId, ): Domain.Contexts.Conversation.Conversation.MessageEntityReference { - // biome-ignore lint/complexity/useLiteralKeys: metadata is an index signature requiring bracket notation + // biome-ignore lint/complexity/useLiteralKeys: metadata is Record requiring bracket notation per TS4111 const messagingId = (messagingMessage.metadata?.['originalSid'] as string) || messagingMessage.id; diff --git a/packages/sthrift/persistence/src/datasources/readonly/user/admin-user/admin-user.read-repository.test.ts b/packages/sthrift/persistence/src/datasources/readonly/user/admin-user/admin-user.read-repository.test.ts index 0ba9eb2b2..bad9978a3 100644 --- a/packages/sthrift/persistence/src/datasources/readonly/user/admin-user/admin-user.read-repository.test.ts +++ b/packages/sthrift/persistence/src/datasources/readonly/user/admin-user/admin-user.read-repository.test.ts @@ -160,7 +160,7 @@ test.for(feature, ({ Scenario, Background, BeforeEachScenario }) => { Scenario('Getting all admin users with no results', ({ Given, When, Then }) => { Given('no AdminUser documents exist in the database', () => { - const mockDataSource = repository['mongoDataSource'] as unknown as { + const mockDataSource = repository.mongoDataSource as unknown as { find: ReturnType; }; mockDataSource.find = vi.fn(() => Promise.resolve([])); diff --git a/packages/sthrift/persistence/src/datasources/readonly/user/personal-user/personal-user.read-repository.test.ts b/packages/sthrift/persistence/src/datasources/readonly/user/personal-user/personal-user.read-repository.test.ts index f18751aeb..99542bdac 100644 --- a/packages/sthrift/persistence/src/datasources/readonly/user/personal-user/personal-user.read-repository.test.ts +++ b/packages/sthrift/persistence/src/datasources/readonly/user/personal-user/personal-user.read-repository.test.ts @@ -150,7 +150,7 @@ test.for(feature, ({ Scenario, Background, BeforeEachScenario }) => { Scenario('Getting all personal users with no results', ({ Given, When, Then }) => { Given('no PersonalUser documents exist in the database', () => { - const mockDataSource = repository['mongoDataSource'] as unknown as { + const mockDataSource = repository.mongoDataSource as unknown as { find: ReturnType; }; mockDataSource.find = vi.fn(() => Promise.resolve([]));