A decentralized music tracking and discovery platform built on AT Protocol 🎵

[app] implement search

+362 -85
+8
rocksky-app/bun.lock
··· 6 "dependencies": { 7 "@atproto/api": "^0.15.6", 8 "@expo/vector-icons": "^14.1.0", 9 "@ipld/dag-cbor": "^9.2.2", 10 "@react-native-async-storage/async-storage": "^2.1.2", 11 "@react-native-menu/menu": "^1.2.3", ··· 41 "react": "19.0.0", 42 "react-content-loader": "^7.0.2", 43 "react-dom": "19.0.0", 44 "react-i18next": "^15.5.1", 45 "react-native": "0.79.2", 46 "react-native-gesture-handler": "~2.24.0", ··· 410 411 "@gorhom/portal": ["@gorhom/portal@1.0.14", "", { "dependencies": { "nanoid": "^3.3.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A=="], 412 413 "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], 414 415 "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], ··· 623 "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], 624 625 "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 626 627 "@storybook/addon-actions": ["@storybook/addon-actions@8.6.12", "", { "dependencies": { "@storybook/global": "^5.0.0", "@types/uuid": "^9.0.1", "dequal": "^2.0.2", "polished": "^4.2.2", "uuid": "^9.0.0" }, "peerDependencies": { "storybook": "^8.6.12" } }, "sha512-B5kfiRvi35oJ0NIo53CGH66H471A3XTzrfaa6SxXEJsgxxSeKScG5YeXcCvLiZfvANRQ7QDsmzPUgg0o3hdMXw=="], 628 ··· 1973 "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], 1974 1975 "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], 1976 1977 "react-i18next": ["react-i18next@15.5.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA=="], 1978
··· 6 "dependencies": { 7 "@atproto/api": "^0.15.6", 8 "@expo/vector-icons": "^14.1.0", 9 + "@hookform/resolvers": "^5.0.1", 10 "@ipld/dag-cbor": "^9.2.2", 11 "@react-native-async-storage/async-storage": "^2.1.2", 12 "@react-native-menu/menu": "^1.2.3", ··· 42 "react": "19.0.0", 43 "react-content-loader": "^7.0.2", 44 "react-dom": "19.0.0", 45 + "react-hook-form": "^7.56.4", 46 "react-i18next": "^15.5.1", 47 "react-native": "0.79.2", 48 "react-native-gesture-handler": "~2.24.0", ··· 412 413 "@gorhom/portal": ["@gorhom/portal@1.0.14", "", { "dependencies": { "nanoid": "^3.3.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A=="], 414 415 + "@hookform/resolvers": ["@hookform/resolvers@5.0.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA=="], 416 + 417 "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], 418 419 "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], ··· 627 "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], 628 629 "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 630 + 631 + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], 632 633 "@storybook/addon-actions": ["@storybook/addon-actions@8.6.12", "", { "dependencies": { "@storybook/global": "^5.0.0", "@types/uuid": "^9.0.1", "dequal": "^2.0.2", "polished": "^4.2.2", "uuid": "^9.0.0" }, "peerDependencies": { "storybook": "^8.6.12" } }, "sha512-B5kfiRvi35oJ0NIo53CGH66H471A3XTzrfaa6SxXEJsgxxSeKScG5YeXcCvLiZfvANRQ7QDsmzPUgg0o3hdMXw=="], 634 ··· 1979 "react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="], 1980 1981 "react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="], 1982 + 1983 + "react-hook-form": ["react-hook-form@7.56.4", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-Rob7Ftz2vyZ/ZGsQZPaRdIefkgOSrQSPXfqBdvOPwJfoGnjwRJUs7EM7Kc1mcoDv3NOtqBzPGbcMB8CGn9CKgw=="], 1984 1985 "react-i18next": ["react-i18next@15.5.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA=="], 1986
+2
rocksky-app/package.json
··· 21 "dependencies": { 22 "@atproto/api": "^0.15.6", 23 "@expo/vector-icons": "^14.1.0", 24 "@ipld/dag-cbor": "^9.2.2", 25 "@react-native-async-storage/async-storage": "^2.1.2", 26 "@react-native-menu/menu": "^1.2.3", ··· 56 "react": "19.0.0", 57 "react-content-loader": "^7.0.2", 58 "react-dom": "19.0.0", 59 "react-i18next": "^15.5.1", 60 "react-native": "0.79.2", 61 "react-native-gesture-handler": "~2.24.0",
··· 21 "dependencies": { 22 "@atproto/api": "^0.15.6", 23 "@expo/vector-icons": "^14.1.0", 24 + "@hookform/resolvers": "^5.0.1", 25 "@ipld/dag-cbor": "^9.2.2", 26 "@react-native-async-storage/async-storage": "^2.1.2", 27 "@react-native-menu/menu": "^1.2.3", ··· 57 "react": "19.0.0", 58 "react-content-loader": "^7.0.2", 59 "react-dom": "19.0.0", 60 + "react-hook-form": "^7.56.4", 61 "react-i18next": "^15.5.1", 62 "react-native": "0.79.2", 63 "react-native-gesture-handler": "~2.24.0",
+10
rocksky-app/src/api/search.ts
···
··· 1 + import axios from "axios"; 2 + import { API_URL } from "../consts"; 3 + import { SearchResponse } from "../types/search"; 4 + 5 + export const search = async (query: string) => { 6 + const response = await axios.get<SearchResponse>( 7 + `${API_URL}/search?q=${query}&size=100` 8 + ); 9 + return response.data; 10 + };
+8
rocksky-app/src/hooks/useSearch.tsx
···
··· 1 + import { useMutation } from "@tanstack/react-query"; 2 + import { search } from "../api/search"; 3 + import { SearchResponse } from "../types/search"; 4 + 5 + export const useSearchMutation = () => 6 + useMutation<SearchResponse, Error, string>({ 7 + mutationFn: (query: string) => search(query), 8 + });
+8 -1
rocksky-app/src/screens/Search/Search.stories.tsx
··· 21 type Story = StoryObj<typeof meta>; 22 23 export const Basic: Story = { 24 - args: {}, 25 };
··· 21 type Story = StoryObj<typeof meta>; 22 23 export const Basic: Story = { 24 + args: { 25 + onSubmit: (query: string) => {}, 26 + onPressAlbum: (uri: string) => {}, 27 + onPressArtist: (uri: string) => {}, 28 + onPressTrack: (uri: string) => {}, 29 + onPressUser: (handle: string) => {}, 30 + results: [], 31 + }, 32 };
+146 -65
rocksky-app/src/screens/Search/Search.tsx
··· 1 import Song from "@/src/components/Song"; 2 import StickyPlayer from "@/src/components/StickyPlayer"; 3 - import { ScrollView, View } from "react-native"; 4 import SearchInput from "./SearchInput"; 5 6 - const tracks = [ 7 - { 8 - title: "Work from Home (feat. Ty Dolla $ign)", 9 - artist: "Fith Harmony", 10 - image: 11 - "https://cdn.rocksky.app/covers/cbed73745681d6a170b694ee11bb527c.jpg", 12 - }, 13 - { 14 - title: "BED", 15 - artist: "Joel Corry", 16 - image: "https://i.scdn.co/image/ab67616d0000b273b06c09b9f72389ee7f1cbd6b", 17 - }, 18 - { 19 - title: "Friday (feat. Mufasa & Hypeman) - Dopamine Re-Edit", 20 - artist: "Riton", 21 - image: 22 - "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:7vdlgi2bflelz7mmuxoqjfcr/bafkreihvqfivhiaxj4cxkwfptzdxwnrwkqsasuirxi5ub3mgyxzgi4yc7i@jpeg", 23 - }, 24 - { 25 - title: "Hear Me Say", 26 - artist: "Jonas Blue", 27 - image: 28 - "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:7vdlgi2bflelz7mmuxoqjfcr/bafkreigjwz3opdzwdeispeckym2gpiy4kixp4skz2gdelckujkbnkz3edm@jpeg", 29 - }, 30 - { 31 - title: "Flowers (feat. Jaykae and MALIKA)", 32 - artist: "Nathan Dawe", 33 - image: 34 - "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:7vdlgi2bflelz7mmuxoqjfcr/bafkreigqdrg5pmv7ji7f72ricgqjyneno5j5qzeppsqmoy3yek3wqv3fca@jpeg", 35 - }, 36 - { 37 - title: "Sorry", 38 - artist: "Joel Corry", 39 - image: 40 - "https://cdn.rocksky.app/covers/ec9bbc208b04182f315f8137cfb2125b.jpg", 41 - }, 42 - { 43 - title: "3 Libras", 44 - artist: "A Perfect Circle", 45 - image: "https://i.scdn.co/image/ab67616d0000b2732d73b494efcb99356f8c7b28", 46 - }, 47 - ]; 48 49 - const Search = () => { 50 return ( 51 - <View className="w-full h-full bg-black"> 52 - <SearchInput className="mt-[50px] ml-[15px] mr-[15px]" /> 53 - <ScrollView className="h-[99%] w-full mt-[10px] pl-[15px] pr-[15px]"> 54 - {tracks.map((song, index) => ( 55 - <Song 56 - key={index} 57 - image={song.image} 58 - title={song.title} 59 - artist={song.artist} 60 - size={60} 61 - className="mt-[10px]" 62 - onPress={() => {}} 63 - onPressAlbum={() => {}} 64 - did="" 65 - albumUri="" 66 /> 67 - ))} 68 - </ScrollView> 69 - <View className="w-full absolute bottom-0 bg-black"> 70 - <StickyPlayer /> 71 - </View> 72 - </View> 73 ); 74 }; 75
··· 1 + import Album from "@/src/components/Album"; 2 + import Artist from "@/src/components/Artist"; 3 + import ScrollToTopButton from "@/src/components/ScrollToTopButton"; 4 import Song from "@/src/components/Song"; 5 import StickyPlayer from "@/src/components/StickyPlayer"; 6 + import useScrollToTop from "@/src/hooks/useScrollToTop"; 7 + import { useNowPlayingContext } from "@/src/providers/NowPlayingProvider"; 8 + import { FC } from "react"; 9 + import { 10 + ActivityIndicator, 11 + Keyboard, 12 + KeyboardAvoidingView, 13 + Platform, 14 + ScrollView, 15 + TouchableWithoutFeedback, 16 + View, 17 + } from "react-native"; 18 import SearchInput from "./SearchInput"; 19 20 + export type SearchProps = { 21 + onSubmit: (query: string) => void; 22 + results: { 23 + record: any; 24 + table: string; 25 + }[]; 26 + isLoading?: boolean; 27 + onPressAlbum: (uri: string) => void; 28 + onPressArtist: (uri: string) => void; 29 + onPressTrack: (uri: string) => void; 30 + onPressUser: (handle: string) => void; 31 + }; 32 + 33 + const Search: FC<SearchProps> = (props) => { 34 + const { 35 + results, 36 + onSubmit, 37 + isLoading, 38 + onPressAlbum, 39 + onPressArtist, 40 + onPressTrack, 41 + onPressUser, 42 + } = props; 43 + const { scrollToTop, isVisible, fadeAnim, handleScroll, scrollViewRef } = 44 + useScrollToTop(); 45 + const nowPlaying = useNowPlayingContext(); 46 + const bottomButtonPosition = nowPlaying ? 80 : 20; 47 48 return ( 49 + <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}> 50 + <KeyboardAvoidingView 51 + className="flex-1" 52 + behavior={Platform.OS === "ios" ? "padding" : undefined} 53 + > 54 + <View className="w-full h-full bg-black"> 55 + <SearchInput 56 + className="mt-[50px] ml-[15px] mr-[15px]" 57 + onSubmit={onSubmit} 58 /> 59 + {isLoading && ( 60 + <View className="w-fullitems-center"> 61 + <ActivityIndicator 62 + size="large" 63 + color="#bdbaba" 64 + className="mt-[50px]" 65 + /> 66 + </View> 67 + )} 68 + {!isLoading && ( 69 + <ScrollView 70 + ref={scrollViewRef} 71 + onScroll={handleScroll} 72 + className="w-full mt-[10px] pl-[15px] pr-[15px]" 73 + showsVerticalScrollIndicator={false} 74 + removeClippedSubviews={true} 75 + keyboardShouldPersistTaps="handled" 76 + > 77 + {results.map(({ record, table }, index) => { 78 + switch (table) { 79 + case "tracks": 80 + return ( 81 + <Song 82 + key={index} 83 + image={record.album_art} 84 + title={record.title} 85 + artist={record.artist} 86 + size={60} 87 + className="mt-[10px]" 88 + onPress={() => onPressTrack(record.uri)} 89 + onPressAlbum={() => onPressTrack(record.uri)} 90 + did="" 91 + albumUri={record.album_uri} 92 + /> 93 + ); 94 + case "albums": 95 + return ( 96 + <Album 97 + key={index} 98 + image={record.album_art} 99 + title={record.title} 100 + artist={record.artist} 101 + size={60} 102 + className="mt-[10px]" 103 + onPress={() => onPressAlbum(record.uri)} 104 + did="" 105 + row 106 + /> 107 + ); 108 + case "artists": 109 + return ( 110 + <Artist 111 + key={index} 112 + image={record.picture} 113 + name={record.name} 114 + size={60} 115 + className="mt-[10px]" 116 + onPress={() => onPressArtist(record.uri)} 117 + did="" 118 + row 119 + /> 120 + ); 121 + case "users": 122 + return ( 123 + <Artist 124 + key={index} 125 + image={record.avatar} 126 + name={record.display_name} 127 + size={60} 128 + className="mt-[10px]" 129 + onPress={() => onPressUser(record.handle)} 130 + did={record.did} 131 + row 132 + /> 133 + ); 134 + default: 135 + break; 136 + } 137 + })} 138 + </ScrollView> 139 + )} 140 + {isVisible && ( 141 + <ScrollToTopButton 142 + fadeAnim={fadeAnim} 143 + bottom={bottomButtonPosition} 144 + onPress={scrollToTop} 145 + /> 146 + )} 147 + 148 + <View className="w-full absolute bottom-0 bg-black"> 149 + <StickyPlayer /> 150 + </View> 151 + </View> 152 + </KeyboardAvoidingView> 153 + </TouchableWithoutFeedback> 154 ); 155 }; 156
+49 -18
rocksky-app/src/screens/Search/SearchInput/SearchInput.tsx
··· 1 import Feather from "@expo/vector-icons/Feather"; 2 import Ionicons from "@expo/vector-icons/Ionicons"; 3 - import { FC } from "react"; 4 import { TextInput, View } from "react-native"; 5 6 export type SearchInputProps = { 7 className?: string; 8 }; 9 10 const SearchInput: FC<SearchInputProps> = (props) => { 11 - const { className } = props; 12 return ( 13 - <> 14 - <View className={`relative ${className}`}> 15 - <Feather 16 - className="absolute z-10 top-[23px] left-[10px]" 17 - name="search" 18 - size={24} 19 - color="#313131" 20 - /> 21 - <TextInput 22 - className="font-rockford-medium bg-[#fff] text-[#000] rounded-[2px] p-[10px] mt-[10px] h-[49px] text-[18px] pl-[46px] pr-[46px]" 23 - placeholder="Search" 24 - placeholderTextColor="#A0A0A0" 25 - cursorColor="#000" 26 - /> 27 <Ionicons 28 className="absolute z-10 top-[23px] right-[10px]" 29 name="close" 30 size={24} 31 color="#313131" 32 /> 33 - </View> 34 - </> 35 ); 36 }; 37
··· 1 import Feather from "@expo/vector-icons/Feather"; 2 import Ionicons from "@expo/vector-icons/Ionicons"; 3 + import { zodResolver } from "@hookform/resolvers/zod"; 4 + import { FC, useEffect } from "react"; 5 + import { useForm } from "react-hook-form"; 6 import { TextInput, View } from "react-native"; 7 + import { z } from "zod"; 8 9 export type SearchInputProps = { 10 className?: string; 11 + onSubmit?: (value: string) => void; 12 }; 13 14 + const schema = z.object({ 15 + search: z.string().min(1, "Please enter a search term"), 16 + }); 17 + 18 + type FormData = z.infer<typeof schema>; 19 + 20 const SearchInput: FC<SearchInputProps> = (props) => { 21 + const { className, onSubmit } = props; 22 + const { register, setValue, handleSubmit, watch } = useForm<FormData>({ 23 + resolver: zodResolver(schema), 24 + defaultValues: { search: "" }, 25 + }); 26 + 27 + // Register input on mount 28 + useEffect(() => { 29 + register("search"); 30 + }, [register]); 31 + 32 return ( 33 + <View className={`relative ${className}`}> 34 + <Feather 35 + className="absolute z-10 top-[23px] left-[10px]" 36 + name="search" 37 + size={24} 38 + color="#313131" 39 + /> 40 + <TextInput 41 + className="font-rockford-medium bg-[#fff] text-[#000] rounded-[2px] p-[10px] mt-[10px] h-[49px] text-[18px] pl-[46px] pr-[46px]" 42 + placeholder="Search" 43 + placeholderTextColor="#A0A0A0" 44 + cursorColor="#000" 45 + onChangeText={(text) => { 46 + setValue("search", text, { shouldValidate: true }); 47 + handleSubmit((data) => onSubmit?.(data.search))(); 48 + }} 49 + onSubmitEditing={handleSubmit((data) => onSubmit?.(data.search.trim()))} 50 + value={watch("search")} 51 + returnKeyType="search" 52 + /> 53 + {watch("search")?.length > 0 && ( 54 <Ionicons 55 className="absolute z-10 top-[23px] right-[10px]" 56 name="close" 57 size={24} 58 color="#313131" 59 + onPress={() => { 60 + setValue("search", ""); 61 + onSubmit?.(""); 62 + }} 63 /> 64 + )} 65 + </View> 66 ); 67 }; 68
+63
rocksky-app/src/screens/Search/SearchWithData.tsx
···
··· 1 + import { useSearchMutation } from "@/src/hooks/useSearch"; 2 + import { RootStackParamList } from "@/src/Navigation"; 3 + import { useNavigation } from "@react-navigation/native"; 4 + import { NativeStackNavigationProp } from "@react-navigation/native-stack"; 5 + import _ from "lodash"; 6 + import * as R from "ramda"; 7 + import { useEffect, useState } from "react"; 8 + import Search from "./Search"; 9 + 10 + const SearchWithData = () => { 11 + const navigation = 12 + useNavigation<NativeStackNavigationProp<RootStackParamList>>(); 13 + const [results, setResults] = useState< 14 + { 15 + record: any; 16 + table: string; 17 + }[] 18 + >([]); 19 + const { mutate, data, isPending } = useSearchMutation(); 20 + 21 + const handleSearch = (query: string) => { 22 + if (query.length < 3) { 23 + setResults([]); 24 + return; 25 + } 26 + 27 + if (query.length < 3) { 28 + return; 29 + } 30 + 31 + _.debounce(() => { 32 + mutate(query); 33 + }, 500)(); 34 + }; 35 + 36 + useEffect(() => { 37 + if (!data) return; 38 + 39 + setResults( 40 + R.pipe( 41 + R.prop("records"), 42 + R.sort(R.descend((item) => R.path(["record", "xata_score"], item) ?? 0)) 43 + )(data) as { 44 + record: any; 45 + table: string; 46 + }[] 47 + ); 48 + }, [data]); 49 + 50 + return ( 51 + <Search 52 + onSubmit={handleSearch} 53 + results={results} 54 + isLoading={isPending} 55 + onPressAlbum={(uri) => navigation.navigate("AlbumDetails", { uri })} 56 + onPressArtist={(uri) => navigation.navigate("ArtistDetails", { uri })} 57 + onPressTrack={(uri) => navigation.navigate("SongDetails", { uri })} 58 + onPressUser={(handle) => navigation.navigate("UserProfile", { handle })} 59 + /> 60 + ); 61 + }; 62 + 63 + export default SearchWithData;
+1 -1
rocksky-app/src/screens/Search/index.tsx
··· 1 - import Search from "./Search"; 2 3 export default Search;
··· 1 + import Search from "./SearchWithData"; 2 3 export default Search;
+67
rocksky-app/src/types/search.ts
···
··· 1 + export interface XataHighlight { 2 + name?: string[]; 3 + title?: string[]; 4 + } 5 + 6 + export interface BaseRecord { 7 + sha256: string; 8 + uri: string; 9 + xata_createdat: string; 10 + xata_highlight: XataHighlight; 11 + xata_id: string; 12 + xata_score: number; 13 + xata_table: string; 14 + xata_updatedat: string; 15 + xata_version: number; 16 + apple_music_link: string | null; 17 + spotify_link: string | null; 18 + tidal_link: string | null; 19 + youtube_link: string | null; 20 + } 21 + 22 + export interface ArtistRecord extends BaseRecord { 23 + name: string; 24 + picture: string; 25 + biography: string | null; 26 + born: string | null; 27 + born_in: string | null; 28 + died: string | null; 29 + } 30 + 31 + export interface TrackRecord extends BaseRecord { 32 + album: string; 33 + album_art: string; 34 + album_artist: string; 35 + album_uri: string; 36 + artist: string; 37 + artist_uri: string; 38 + disc_number: number; 39 + duration: number; 40 + title: string; 41 + track_number: number; 42 + composer: string | null; 43 + copyright_message: string | null; 44 + genre: string | null; 45 + label: string | null; 46 + lyrics: string | null; 47 + mb_id: string | null; 48 + } 49 + 50 + export interface AlbumRecord extends BaseRecord { 51 + title: string; 52 + album_art: string; 53 + artist: string; 54 + artist_uri: string; 55 + release_date: string; 56 + year: number; 57 + } 58 + 59 + export type RecordItem = 60 + | { table: "artists"; record: ArtistRecord } 61 + | { table: "tracks"; record: TrackRecord } 62 + | { table: "albums"; record: AlbumRecord }; 63 + 64 + export interface SearchResponse { 65 + totalCount: number; 66 + records: RecordItem[]; 67 + }