Skip to content

Commit

Permalink
[Feature][Search Screen] 검색 페이지 기능 구현. (#262)
Browse files Browse the repository at this point in the history
* style: searchicon added to main screen

* noop search text input

* feedlist in searchscreen rudimentary implementation done

* search challenge ui implemented

* styles added

* search user screen implemented
  • Loading branch information
akdlsz21 authored Nov 5, 2023
1 parent cfa5dda commit 6198b44
Show file tree
Hide file tree
Showing 14 changed files with 463 additions and 6 deletions.
9 changes: 9 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import ChallengeEditScreen from './src/screens/Main/challenge/ChallengeEditScree
import SubjectEditScreen from './src/screens/Main/challenge/SubjectEditScreen';
import GoalEditScreen from './src/screens/Main/challenge/GoalEditScreen';
import { getExpoToken, setExpoToken } from './src/utils/hooks/asyncStorage/Login';
import SearchScreen from './src/screens/search/SearchScreen';

const Stack = createStackNavigator<RootStackParamList>();

Expand Down Expand Up @@ -229,6 +230,14 @@ export default function App() {
cardStyle: { backgroundColor: 'white' },
}}
/>
<Stack.Screen
name={'Search'}
component={SearchScreen}
options={{
...headerOptions,
cardStyle: { backgroundColor: 'white' },
}}
/>
<Stack.Screen
name={'Settings'}
component={SettingsScreen}
Expand Down
2 changes: 2 additions & 0 deletions src/components/profile/ChallengeProfileCurrent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const CurrentChallengeProfile = ({
? challengeDescription?.slice(0, 21) + '...'
: challengeDescription
: '';
console.log('insightNumber', insightNumber);

return (
<Pressable
onPress={() =>
Expand Down
11 changes: 11 additions & 0 deletions src/constants/Icons/search/SearchIconXml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_10404_24817)">
<path d="M20.6894 18.992L16.6246 14.9272C17.5819 13.6584 18.1563 12.0861 18.1563 10.378C18.1563 6.19934 14.7559 2.7998 10.5782 2.7998C6.39954 2.80069 3 6.20023 3 10.3788C3 14.5566 6.39954 17.957 10.5782 17.957C12.2854 17.957 13.8586 17.3826 15.1273 16.4253L19.1921 20.4901C19.3986 20.6966 19.6695 20.7998 19.9403 20.7998C20.2112 20.7998 20.4821 20.6966 20.6894 20.4892C21.1023 20.0763 21.1023 19.4058 20.6894 18.992ZM5.11754 10.3788C5.11754 7.36752 7.56683 4.91823 10.5782 4.91823C13.5886 4.91823 16.0379 7.36752 16.0379 10.378C16.0379 13.3884 13.5886 15.8377 10.5782 15.8377C7.56683 15.8386 5.11754 13.3893 5.11754 10.3788Z" fill="#121314" fill-opacity="0.8"/>
</g>
<defs>
<clipPath id="clip0_10404_24817">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>
`;
Binary file added src/screens/Feed/.FeedList.tsx.swp
Binary file not shown.
6 changes: 4 additions & 2 deletions src/screens/Feed/FeedItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ import { StackNavigationProp } from '@react-navigation/stack';
interface FeedItemProps {
insight: InsightData;
localId?: string;
onBookMarkClick: (id: number) => void;
onBookMarkClick?: (id: number) => void;
}

const FeedItem = ({ insight, localId, onBookMarkClick }: FeedItemProps) => {
const { id, contents, createdAt, link, reaction, writer, bookmark } = insight;
const navigation = useNavigation<StackNavigationProp<any>>();
const handleOnBookMarkPress = () => {
onBookMarkClick(id);
if (onBookMarkClick) {
onBookMarkClick(id);
}
};

const handleProfilePress = () => {
Expand Down
2 changes: 1 addition & 1 deletion src/screens/Feed/FeedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useGetUserId } from '../../utils/hooks/useGetUserId';
interface FeedListProps {
feedList: InfiniteData<InsightData[] | undefined> | undefined;
fetchNextPage: () => void;
touchBookMark: UseMutateFunction<void, unknown, number, unknown>;
touchBookMark?: UseMutateFunction<void, unknown, number, unknown>;
upperComponent?: React.ReactNode;
feedListQueryClient?: QueryClient;
feedListIsLoading: boolean;
Expand Down
18 changes: 15 additions & 3 deletions src/screens/Main/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import alarmExist from '../../constants/Icons/alarm/alarmExist';
import { useQuery } from '@tanstack/react-query';
import { notificationApi, notificationKeys } from '../../utils/api/notification/notification';
import { ChallengeAPI } from '../../utils/api/ChallengeAPI';
import { View } from '../../components/Themed';
import SearchIconXml from '../../constants/Icons/search/SearchIconXml';

const Tab = createBottomTabNavigator();

Expand Down Expand Up @@ -84,9 +86,19 @@ const Tabs = ({ navigation, route }) => {
headerShadowVisible: false,
headerTitle: '',
headerRight: () => (
<Pressable onPress={() => navigation.navigate('Notification')}>
<SvgXml style={{ marginRight: 16 }} xml={data?.exist ? alarmExist : alarmEmpty} />
</Pressable>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
}}
>
<Pressable onPress={() => navigation.navigate('Notification')}>
<SvgXml style={{ marginRight: 16 }} xml={data?.exist ? alarmExist : alarmEmpty} />
</Pressable>
<Pressable onPress={() => navigation.navigate('Search')}>
<SvgXml style={{ marginRight: 16 }} xml={SearchIconXml} />
</Pressable>
</View>
),
}}
sceneContainerStyle={{ backgroundColor: 'white' }}
Expand Down
119 changes: 119 additions & 0 deletions src/screens/search/SearchChallengeScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { ListRenderItemInfo, StyleSheet, Text, View } from 'react-native';
import React, { useContext } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { SearchContext } from './SearchScreen';
import httpClient from '../../utils/api/BaseHttpClient';
import { SafeAreaView } from 'react-native-safe-area-context';
import { FlatList } from 'react-native-gesture-handler';
import CurrentChallengeProfile from '../../components/profile/ChallengeProfileCurrent';
import { ActivityIndicator } from 'react-native-paper';

export interface SearchChallenge {
id: number;
insightCnt: number;
interestName: string;
introduction: string;
name: string;
}

const fetchSearchChallenges = async ({ searchText = 'test', page, limit }) => {
const urlSearchParams = new URLSearchParams();
urlSearchParams.append('keyword', searchText);
urlSearchParams.append('searchType', 'CHALLENGE');
urlSearchParams.append('limit', limit.toString());
if (page > 0) {
urlSearchParams.append('cursor', page.toString());
}

const queryString = urlSearchParams.toString();
const requestUrl = `https://api-keewe.com/api/v1/search?${queryString}`;
console.log('Request URL:', requestUrl); // Debugging: Log the full request URL

try {
const response = await httpClient.get<SearchChallenge[]>(requestUrl);
return response.data;
} catch (error) {
console.error('search insight screen', error);
throw error;
}
};

const SearchChallengeScreen = () => {
const { searchText } = useContext(SearchContext);

const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useInfiniteQuery({
queryKey: ['searchScreen', 'challenge', searchText],
queryFn: ({ pageParam = 0 }) =>
fetchSearchChallenges({ searchText, page: pageParam, limit: 10 }),
getNextPageParam: (lastPage) => {
return lastPage[lastPage.length - 1]?.id;
},
});

const onEndReached = () => {
if (hasNextPage) {
fetchNextPage();
}
};

const renderItem = ({ item, index }: ListRenderItemInfo<SearchChallenge>) => {
console.log('renderItem', item);
return (
<CurrentChallengeProfile
key={index}
name={item.name}
challengeId={item.id}
interest={item.interestName}
challengeDescription={item.name}
insightNumber={String(item.insightCnt)}
participate={false}
/>
);
};

if (status === 'loading') {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" />
</View>
);
}

if (status === 'error') {
return (
<View style={styles.centered}>
<Text>Failed to load challenges.</Text>
</View>
);
}

const flatListData = data?.pages.flatMap((page) => page) || [];

return (
<SafeAreaView style={{ flex: 1, backgroundColor: 'white' }}>
<FlatList
data={flatListData}
renderItem={(info) => renderItem(info)}
keyExtractor={(item, index) => `challenge-${item.id}-${index}`}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
ListFooterComponent={isFetchingNextPage ? <ActivityIndicator size="large" /> : null}
style={styles.list}
/>
</SafeAreaView>
);
};

export default SearchChallengeScreen;

const styles = StyleSheet.create({
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
list: {
paddingBottom: '100%',
marginBottom: 80,
},
});
93 changes: 93 additions & 0 deletions src/screens/search/SearchInsightScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { StyleSheet, Text, View } from 'react-native';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { SearchContext } from './SearchScreen';
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import httpClient from '../../utils/api/BaseHttpClient';
import { RefreshControl, ScrollView } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context';
import { IOScrollView } from 'react-native-intersection-observer';
import FeedList from '../Feed/FeedList';
import { useTheme } from 'react-native-paper';
import { useScrollToTop } from '@react-navigation/native';
import { notificationKeys } from '../../utils/api/notification/notification';
import GoToUploadButton from '../../components/buttons/GoToUploadButton';
import { useInfiniteFeed } from '../../utils/hooks/feedInifiniteScroll/useInfiniteFeed';
interface fetchSearchInsightsParams {
searchText: string;
page: number;
limit: number;
}

const fetchSearchInsights = async ({
searchText = 'test',
page,
limit,
}: fetchSearchInsightsParams) => {
const urlSearchParams = new URLSearchParams();
urlSearchParams.append('keyword', searchText);
urlSearchParams.append('searchType', 'INSIGHT');
urlSearchParams.append('limit', limit.toString());
if (page > 0) {
urlSearchParams.append('cursor', page.toString());
}

const queryString = urlSearchParams.toString();
const requestUrl = `https://api-keewe.com/api/v1/search?${queryString}`;
console.log('Request URL:', requestUrl); // Debugging: Log the full request URL

try {
const response = await httpClient.get<InsightData[]>(requestUrl);
return response.data;
} catch (error) {
console.error('search insight screen', error);
throw error;
}
};

const SearchInsightScreen = () => {
const { searchText } = useContext(SearchContext);

const {
data: feedList,
fetchNextPage,
isLoading: feedListIsLoading,
} = useInfiniteQuery<InsightData[] | undefined>({
queryKey: ['search', searchText],
queryFn: ({ pageParam = 0 }) => fetchSearchInsights({ searchText, page: pageParam, limit: 10 }),
});

const scrollViewRef = useRef(null);

if (feedListIsLoading) {
return (
<View>
<Text>loading</Text>
</View>
);
}
if (!feedList) {
return (
<View>
<Text>feedList is undefined</Text>
</View>
);
}

return (
<SafeAreaView style={{ flex: 1, backgroundColor: 'white' }}>
<IOScrollView ref={scrollViewRef}>
<FeedList
scrollViewRef={scrollViewRef}
feedList={feedList}
fetchNextPage={fetchNextPage}
feedListIsLoading={feedListIsLoading}
/>
</IOScrollView>
<GoToUploadButton />
</SafeAreaView>
);
};

export default SearchInsightScreen;

const styles = StyleSheet.create({});
48 changes: 48 additions & 0 deletions src/screens/search/SearchScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { createContext, useLayoutEffect, useState } from 'react';
import { ScrollView, Text, TextInput } from 'react-native';
import {
MaterialTopTabView,
createMaterialTopTabNavigator,
} from '@react-navigation/material-top-tabs';
import SearchInsightScreen from './SearchInsightScreen';
import SearchUserScreen from './SearchUserScreen';
import SearchChallengeScreen from './SearchChallengeScreen';
import { useTheme } from 'react-native-paper';
// type SearchType = 'INSIGHT' | 'USER' | 'CHALLENGE';

const Tab = createMaterialTopTabNavigator();

// eslint-disable-next-line @typescript-eslint/no-empty-function
export const SearchContext = createContext({ searchText: '', setSearchText: (text: string) => {} });

const SearchScreen = ({ navigation }) => {
const [searchText, setSearchText] = useState('');
const theme = useTheme();
useLayoutEffect(() => {
navigation.setOptions({
headerTitle: () => (
<TextInput value={searchText} onChangeText={setSearchText} placeholder="Search" />
),
});
}, []);

return (
<SearchContext.Provider value={{ searchText, setSearchText }}>
<Tab.Navigator
initialRouteName="게시물"
screenOptions={{
tabBarActiveTintColor: 'black',
tabBarIndicatorStyle: { backgroundColor: theme.colors.brand.primary.main },
tabBarLabelStyle: { fontSize: 14, fontWeight: 'bold' },
tabBarStyle: { backgroundColor: 'white' },
}}
>
<Tab.Screen name="게시물" component={SearchInsightScreen} />
<Tab.Screen name="챌린지" component={SearchChallengeScreen} />
<Tab.Screen name="사용자" component={SearchUserScreen} />
</Tab.Navigator>
</SearchContext.Provider>
);
};

export default SearchScreen;
Loading

0 comments on commit 6198b44

Please sign in to comment.