Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions client/src/lib/components/PostCard/ExerciseFeedbackCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import useExerciseById from '../../content/hooks/useExerciseById';
import styled from 'styled-components/native';
import {COLORS} from '../../../../../shared/src/constants/colors';
import CardSmall from '../Cards/CardSmall';
import {formatContentName} from '../../utils/string';
import TouchableOpacity from '../TouchableOpacity/TouchableOpacity';
import {Feedback} from '../../../../../shared/src/types/Feedback';
import FeedbackPostCard from './FeedbackPostCard';

type StyleProps = {
backgroundColor: string;
};

const Wrapper = styled(TouchableOpacity)<StyleProps>(({backgroundColor}) => ({
flex: 1,
backgroundColor,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderBottomLeftRadius: 24,
borderBottomRightRadius: 24,
}));

const ExerciseCard = styled(CardSmall)<StyleProps>(({backgroundColor}) => ({
backgroundColor,
}));

type Props = {
feedbackPost: Feedback;
clip?: boolean;
backgroundColor?: string;
onPress?: () => void;
};

const ExerciseFeedbackCard: React.FC<Props> = ({
onPress,
feedbackPost,
clip,
backgroundColor = COLORS.WHITE,
}) => {
const exercise = useExerciseById(
feedbackPost.exerciseId,
feedbackPost.language,
);

if (!exercise) return null;

return (
<Wrapper onPress={onPress} backgroundColor={backgroundColor}>
<ExerciseCard
title={formatContentName(exercise)}
cardStyle={exercise.card}
backgroundColor={backgroundColor}
/>
<FeedbackPostCard
feedbackPost={feedbackPost}
clip={clip}
backgroundColor={backgroundColor}
/>
</Wrapper>
);
};

export default ExerciseFeedbackCard;
23 changes: 23 additions & 0 deletions client/src/lib/sessions/api/sessions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {isNil, reject} from 'ramda';
import {LiveSessionType} from '../../../../../shared/src/schemas/Session';
import apiClient from '../../apiClient/apiClient';
import {Feedback} from '../../../../../shared/src/types/Feedback';

const SESSIONS_ENDPOINT = '/sessions';

Expand All @@ -24,3 +25,25 @@ export const fetchSessions = async (
throw new Error('Could not fetch sessions', {cause});
}
};

export const fetchHostFeedback = async (
limit?: number,
): Promise<Feedback[]> => {
try {
const queryParams = new URLSearchParams(
reject(isNil, {limit: limit?.toString()}),
);

const response = await apiClient(
`${SESSIONS_ENDPOINT}/hostFeedback?${queryParams}`,
);
if (!response.ok) {
throw new Error(await response.text());
}

return response.json();
} catch (cause) {
console.log(cause);
throw new Error('Could not fetch host feedback', {cause});
}
};
18 changes: 18 additions & 0 deletions client/src/lib/sessions/hooks/useHostFeedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {useCallback, useState} from 'react';
import {Feedback} from '../../../../../shared/src/types/Feedback';
import {fetchHostFeedback} from '../api/sessions';

const useHostFeedback = () => {
const [feedback, setFeedback] = useState<Feedback[]>([]);

const fetchFeedback = useCallback(async () => {
setFeedback(await fetchHostFeedback(20));
}, [setFeedback]);

return {
hostFeedback: feedback,
fetchHostFeedback: fetchFeedback,
};
};

export default useHostFeedback;
22 changes: 20 additions & 2 deletions client/src/routes/screens/Home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import RecommendedSessions from './components/RecommendedSessions';
import WelcomeBanner from './components/WelcomeBanner';
import GetStartedBanner from './components/GetStartedBanner';
import Gutters from '../../../lib/components/Gutters/Gutters';
import useHostFeedback from '../../../lib/sessions/hooks/useHostFeedback';
import HostFeedback from './components/HostFeedback';

const Map = styled(AnimatedLottieView).attrs({
source: todayMap,
Expand Down Expand Up @@ -92,6 +94,7 @@ const Home = () => {
const [isLoading, setIsLoading] = useState(true);
const recommendedSessions = useRecommendedSessions();
const {fetchSessions, sessions} = useSessions();
const {fetchHostFeedback, hostFeedback} = useHostFeedback();
const {fetchSharingPosts, sharingPosts} = useSharingPosts();

const otherSessions = useMemo(
Expand All @@ -104,10 +107,14 @@ const Home = () => {
);

const fetch = useCallback(() => {
Promise.all([fetchSessions(), fetchSharingPosts()]).finally(() => {
Promise.all([
fetchSessions(),
fetchSharingPosts(),
fetchHostFeedback(),
]).finally(() => {
setIsLoading(false);
});
}, [fetchSessions, fetchSharingPosts, setIsLoading]);
}, [fetchSessions, fetchSharingPosts, fetchHostFeedback, setIsLoading]);

useThrottledFocusEffect(fetch);

Expand Down Expand Up @@ -159,6 +166,17 @@ const Home = () => {
<Spacer16 />
</>
)}
{!isLoading && hostFeedback.length > 0 && (
<StickyHeading>
<Heading16>{t('sections.hostFeedback')}</Heading16>
</StickyHeading>
)}
{!isLoading && hostFeedback.length > 0 && (
<>
<HostFeedback feedback={hostFeedback} />
<Spacer16 />
</>
)}
{!isLoading && sharingPosts.length > 0 && (
<StickyHeading>
<Heading16>{t('sections.community')}</Heading16>
Expand Down
70 changes: 70 additions & 0 deletions client/src/routes/screens/Home/components/HostFeedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, {useCallback} from 'react';
import {Dimensions, FlatList, ListRenderItem} from 'react-native';
import styled from 'styled-components/native';
import {useNavigation} from '@react-navigation/native';
import {NativeStackNavigationProp} from '@react-navigation/native-stack';
import {Spacer16} from '../../../../lib/components/Spacers/Spacer';
import {SPACINGS} from '../../../../lib/constants/spacings';
import {ModalStackProps} from '../../../../lib/navigation/constants/routes';
import Gutters from '../../../../lib/components/Gutters/Gutters';
import {Feedback} from '../../../../../../shared/src/types/Feedback';
import ExerciseFeedbackCard from '../../../../lib/components/PostCard/ExerciseFeedbackCard';

const SCREEN_DIMENSIONS = Dimensions.get('screen');
const CARD_WIDTH = SCREEN_DIMENSIONS.width * 0.6;
const CARD_HEIGHT = SCREEN_DIMENSIONS.height / 3;

const FeedbackWrapper = styled.View({
width: CARD_WIDTH,
height: CARD_HEIGHT,
});

const FeedbackCard: React.FC<{feedbackPost: Feedback}> = ({feedbackPost}) => {
const {navigate} =
useNavigation<NativeStackNavigationProp<ModalStackProps>>();

const onPress = useCallback(() => {
navigate('FeedbackPostModal', {
feedbackPost,
});
}, [navigate, feedbackPost]);

return (
<FeedbackWrapper>
<ExerciseFeedbackCard
feedbackPost={feedbackPost}
onPress={onPress}
clip
/>
</FeedbackWrapper>
);
};

const renderSharingPost: ListRenderItem<Feedback> = ({item}) => (
<FeedbackCard feedbackPost={item} />
);

type Props = {
feedback: Feedback[];
};
const HostFeedback: React.FC<Props> = ({feedback}) =>
feedback.length === 1 ? (
<Gutters>
<FeedbackCard feedbackPost={feedback[0]} />
</Gutters>
) : (
<FlatList
renderItem={renderSharingPost}
horizontal
data={feedback}
ListHeaderComponent={Spacer16}
ItemSeparatorComponent={Spacer16}
ListFooterComponent={Spacer16}
snapToAlignment="center"
decelerationRate="fast"
snapToInterval={CARD_WIDTH + SPACINGS.SIXTEEN}
showsHorizontalScrollIndicator={false}
/>
);

export default HostFeedback;
5 changes: 4 additions & 1 deletion content/src/ui/Screen.Home.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"sections": {
"forYou": "For you",
"liveSessions": "Live sessions",
"hostFeedback": "Your session host feedback",
"community": "Aware community",
"sharingPosts": "What others are doing"
}
Expand All @@ -28,6 +29,8 @@
"sections": {
"forYou": "För dig",
"liveSessions": "Live-sessioner",
"hostFeedback": "Din sessionvärd-feedback",
"community": "Aware-communityn",
"sharingPosts": "Vad andra gör"
}
},
Expand Down Expand Up @@ -57,4 +60,4 @@
},
"seeAll": "Ver tudo"
}
}
}
18 changes: 18 additions & 0 deletions firestore.indexes.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
{
"indexes": [
{
"collectionGroup": "metricsFeedback",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "sessionId",
"order": "ASCENDING"
},
{
"fieldPath": "host",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "metricsFeedback",
"queryScope": "COLLECTION",
Expand Down
81 changes: 81 additions & 0 deletions functions/src/api/sessions/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const mockGetSessionByHostingCode =
const mockCreateSessionHostingLink =
sessionsController.createSessionHostingLink as jest.Mock;
const mockUpdateSessionHost = sessionsController.updateSessionHost as jest.Mock;
const mockGetSessionsFeedbackByHostId =
feedbackController.getSessionsFeedbackByHostId as jest.Mock;
const mockGetFeedbackCountByExercise =
feedbackController.getFeedbackCountByExercise as jest.Mock;
const mockGetApprovedFeedbackByExercise =
Expand Down Expand Up @@ -260,6 +262,85 @@ describe('/api/sessions', () => {
});
});

describe('GET /hostFeedback', () => {
it('should return host feedback', async () => {
mockGetSessionsFeedbackByHostId.mockResolvedValueOnce([
{
id: 'some-id',
sessionId: 'some-session-id',
exerciseId: 'some-exercise-id',
language: 'en',
question: 'Some question?',
comment: 'Some feedback comment',
answer: true,
completed: true,
sessionType: SessionType.public,
sessionMode: SessionMode.live,
createdAt: Timestamp.fromDate(new Date('2024-10-08T07:24:00.000Z')),
},
{
id: 'some-other-id',
sessionId: 'some-other-session-id',
exerciseId: 'some-exercise-id',
language: 'en',
question: 'Some other question?',
comment: 'Some other feedback comment',
answer: true,
completed: true,
sessionType: SessionType.private,
sessionMode: SessionMode.live,
createdAt: Timestamp.fromDate(new Date('2024-10-08T07:24:00.000Z')),
},
]);

const response = await request(mockServer).get(
'/sessions/hostFeedback?limit=10',
);

expect(mockGetSessionsFeedbackByHostId).toHaveBeenCalledTimes(1);
expect(mockGetSessionsFeedbackByHostId).toHaveBeenCalledWith(
'some-user-id',
10,
);

expect(response.status).toBe(200);
expect(response.body).toMatchObject([
{
id: 'some-id',
sessionId: 'some-session-id',
exerciseId: 'some-exercise-id',
question: 'Some question?',
comment: 'Some feedback comment',
answer: true,
completed: true,
sessionType: SessionType.public,
sessionMode: SessionMode.live,
createdAt: '2024-10-08T07:24:00.000Z',
},
{
id: 'some-other-id',
sessionId: 'some-other-session-id',
exerciseId: 'some-exercise-id',
question: 'Some other question?',
comment: 'Some other feedback comment',
answer: true,
completed: true,
sessionType: SessionType.private,
sessionMode: SessionMode.live,
createdAt: '2024-10-08T07:24:00.000Z',
},
]);
});

it('should return 500 if no limit is set', async () => {
const response = await request(mockServer).get('/sessions/hostFeedback');

expect(mockGetSessionsFeedbackByHostId).toHaveBeenCalledTimes(0);

expect(response.status).toBe(500);
});
});

describe('GET /:id', () => {
it('should return session', async () => {
mockGetSession.mockResolvedValueOnce(
Expand Down
Loading
Loading