search on web
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 19 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 MiB |
@@ -218,8 +218,10 @@ export const background = {
|
||||
playbackBG,
|
||||
playbackBG2,
|
||||
libraryBG,
|
||||
libraryBgWeb: require("./UI/libraryBgWeb.png"),
|
||||
libraryBG2,
|
||||
profileBG,
|
||||
profileBgWeb: require("./UI/profileBgWeb.png"),
|
||||
hitParadeBG,
|
||||
homeBG,
|
||||
homeBGWeb: require("./UI/homeBGWeb.png"),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, Text, TextInput, Image, Platform } from "react-native";
|
||||
import { View, TextInput, Image } from "react-native";
|
||||
import React from "react";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { icons } from "../assets";
|
||||
@@ -20,9 +20,6 @@ const SearchBar = ({
|
||||
gap: 10,
|
||||
...Style.containerRow,
|
||||
}}
|
||||
// experimentalBlurMethod={
|
||||
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
// }
|
||||
>
|
||||
<Image source={icons.search} />
|
||||
<TextInput
|
||||
|
||||
@@ -23,14 +23,17 @@ export default function useDataFromRef({
|
||||
|
||||
format = null,
|
||||
onUpdate = () => null,
|
||||
onError = null,
|
||||
}) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [endReached, setEndReached] = useState(false);
|
||||
const [data, setData] = useState(initialState);
|
||||
const [lastVisible, setLastVisible] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (condition) {
|
||||
setError(null);
|
||||
if (listener && !usePagination) {
|
||||
const subData = getListenerData();
|
||||
return () => subData?.();
|
||||
@@ -57,12 +60,14 @@ export default function useDataFromRef({
|
||||
}
|
||||
setEndReached(false);
|
||||
setLastVisible(null);
|
||||
setError(null);
|
||||
setData(initialState);
|
||||
};
|
||||
|
||||
const getData = async ({ paginate = false } = {}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
let newData = simpleRef ? null : [];
|
||||
let initialDoc = null;
|
||||
|
||||
@@ -115,6 +120,10 @@ export default function useDataFromRef({
|
||||
} else {
|
||||
console.log(e);
|
||||
}
|
||||
setError(e);
|
||||
if (onError) {
|
||||
onError(e);
|
||||
}
|
||||
await updateData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -124,6 +133,7 @@ export default function useDataFromRef({
|
||||
const getListenerData = () => {
|
||||
return ref?.onSnapshot(
|
||||
async (dataSnap) => {
|
||||
setError(null);
|
||||
let newData;
|
||||
|
||||
if (simpleRef) {
|
||||
@@ -145,6 +155,10 @@ export default function useDataFromRef({
|
||||
} else {
|
||||
console.log(e);
|
||||
}
|
||||
setError(e);
|
||||
if (onError) {
|
||||
onError(e);
|
||||
}
|
||||
await updateData([]);
|
||||
setLoading(false);
|
||||
},
|
||||
@@ -160,5 +174,5 @@ export default function useDataFromRef({
|
||||
}
|
||||
};
|
||||
|
||||
return { data, setData, loading, loadMore };
|
||||
return { data, setData, loading, loadMore, hasMore: !endReached, error };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function useDebounced(value, delay = 250) {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
||||
return () => clearTimeout(timer);
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useGlobal } from "reactn";
|
||||
import { usersRef, projectsRef } from "../config/firebase";
|
||||
import useDataFromRef from "./useDataFromRef";
|
||||
|
||||
const batchSizes = {
|
||||
users: 5,
|
||||
projects: 6,
|
||||
};
|
||||
|
||||
const useSearch = () => {
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const queryText = useMemo(() => search.trim(), [search]);
|
||||
const queryLower = useMemo(
|
||||
() => (search || "").toLowerCase().trim(),
|
||||
[search],
|
||||
);
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [autoLoads, setAutoLoads] = useState(0);
|
||||
|
||||
const usersQueryRef = useMemo(() => {
|
||||
try {
|
||||
const lower = queryLower;
|
||||
if (lower) {
|
||||
return usersRef
|
||||
.orderBy("userNameLower")
|
||||
.startAt(lower)
|
||||
.endAt(lower + "\uf8ff");
|
||||
}
|
||||
return usersRef.orderBy("userNameLower");
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}, [queryLower]);
|
||||
|
||||
const projectsQueryRefLower = useMemo(() => {
|
||||
try {
|
||||
const lower = queryText ? queryText.toLowerCase() : "";
|
||||
if (lower) {
|
||||
return projectsRef
|
||||
.orderBy("titleLower")
|
||||
.startAt(lower)
|
||||
.endAt(lower + "\uf8ff");
|
||||
}
|
||||
return null;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}, [queryText]);
|
||||
|
||||
const projectsQueryRefCase = useMemo(() => {
|
||||
try {
|
||||
const raw = queryText || "";
|
||||
if (raw) {
|
||||
return projectsRef
|
||||
.orderBy("title")
|
||||
.startAt(raw)
|
||||
.endAt(raw + "\uf8ff");
|
||||
}
|
||||
return null;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}, [queryText]);
|
||||
|
||||
const {
|
||||
data: userResults = [],
|
||||
loading: usersLoading,
|
||||
loadMore: loadMoreUsers,
|
||||
} = useDataFromRef({
|
||||
ref: usersQueryRef,
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
condition: !!usersQueryRef,
|
||||
refreshArray: [queryLower],
|
||||
usePagination: true,
|
||||
batchSize: batchSizes.users,
|
||||
});
|
||||
|
||||
const filteredUsers = useMemo(() => {
|
||||
const arr = Array.isArray(userResults) ? userResults : [];
|
||||
const matchingUsers = queryLower
|
||||
? arr.filter((user) => {
|
||||
const nameLower = (
|
||||
user?.userNameLower ||
|
||||
user?.userName ||
|
||||
""
|
||||
).toLowerCase();
|
||||
return nameLower.includes(queryLower);
|
||||
})
|
||||
: arr;
|
||||
const seen = new Set();
|
||||
const unique = [];
|
||||
for (const user of matchingUsers) {
|
||||
const id = user?.id;
|
||||
if (!id || seen.has(id)) continue;
|
||||
if (id === currentUID || user?.userID === currentUID) continue;
|
||||
seen.add(id);
|
||||
unique.push(user);
|
||||
}
|
||||
return unique;
|
||||
}, [userResults, queryLower, currentUID]);
|
||||
|
||||
const {
|
||||
data: projectResultsLower = [],
|
||||
loading: projectsLoadingLower,
|
||||
loadMore: loadMoreProjectsLower,
|
||||
} = useDataFromRef({
|
||||
ref: projectsQueryRefLower,
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
condition: !!projectsQueryRefLower,
|
||||
refreshArray: [queryText],
|
||||
usePagination: true,
|
||||
batchSize: batchSizes.projects,
|
||||
});
|
||||
|
||||
const {
|
||||
data: projectResultsCase = [],
|
||||
loading: projectsLoadingCase,
|
||||
loadMore: loadMoreProjectsCase,
|
||||
} = useDataFromRef({
|
||||
ref: projectsQueryRefCase,
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
condition: !!projectsQueryRefCase,
|
||||
refreshArray: [queryText],
|
||||
usePagination: true,
|
||||
batchSize: batchSizes.projects,
|
||||
});
|
||||
|
||||
const {
|
||||
data: projectResultsDefault = [],
|
||||
loading: projectsLoadingDefault,
|
||||
loadMore: loadMoreProjectsDefault,
|
||||
} = useDataFromRef({
|
||||
ref: !queryText ? projectsRef.orderBy("updatedAt", "desc") : null,
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
condition: !queryText,
|
||||
refreshArray: [queryText],
|
||||
usePagination: true,
|
||||
batchSize: batchSizes.projects,
|
||||
});
|
||||
|
||||
const projectsLoading = queryText
|
||||
? projectsLoadingLower || projectsLoadingCase
|
||||
: projectsLoadingDefault;
|
||||
|
||||
const loadMoreProjects = () => {
|
||||
if (queryText) {
|
||||
loadMoreProjectsLower?.();
|
||||
loadMoreProjectsCase?.();
|
||||
} else {
|
||||
loadMoreProjectsDefault?.();
|
||||
}
|
||||
};
|
||||
|
||||
const filteredProjects = useMemo(() => {
|
||||
const lowerMatches = Array.isArray(projectResultsLower)
|
||||
? projectResultsLower
|
||||
: [];
|
||||
const caseMatches = Array.isArray(projectResultsCase)
|
||||
? projectResultsCase
|
||||
: [];
|
||||
const defaultProjects = Array.isArray(projectResultsDefault)
|
||||
? projectResultsDefault
|
||||
: [];
|
||||
|
||||
const arr = queryText ? [...lowerMatches, ...caseMatches] : defaultProjects;
|
||||
const seen = new Set();
|
||||
const unique = [];
|
||||
for (const project of arr) {
|
||||
const id = project?.id;
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
unique.push(project);
|
||||
}
|
||||
return unique;
|
||||
}, [
|
||||
queryText,
|
||||
projectResultsLower,
|
||||
projectResultsCase,
|
||||
projectResultsDefault,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setAutoLoads(0);
|
||||
}, [queryText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
filteredUsers.length < batchSizes.users &&
|
||||
usersQueryRef &&
|
||||
!usersLoading
|
||||
) {
|
||||
if (autoLoads < 3) {
|
||||
setAutoLoads((previous) => previous + 1);
|
||||
loadMoreUsers?.();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
filteredUsers.length,
|
||||
usersLoading,
|
||||
usersQueryRef,
|
||||
autoLoads,
|
||||
loadMoreUsers,
|
||||
]);
|
||||
|
||||
const toggleSelected = (item) => {
|
||||
setSelected((prev) => (prev === item ? null : item));
|
||||
};
|
||||
|
||||
return {
|
||||
search,
|
||||
setSearch,
|
||||
queryText,
|
||||
queryLower,
|
||||
selected,
|
||||
setSelected,
|
||||
toggleSelected,
|
||||
filteredUsers,
|
||||
usersLoading,
|
||||
loadMoreUsers,
|
||||
filteredProjects,
|
||||
projectsLoading,
|
||||
loadMoreProjects,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSearch;
|
||||
@@ -183,7 +183,10 @@ const HitParade = () => {
|
||||
const isWeb = Platform.OS === "web";
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.hitParadeBG} headerType="NONE">
|
||||
<Page
|
||||
backgroundImg={isWeb ? background.libraryBgWeb : background.hitParadeBG}
|
||||
headerType="NONE"
|
||||
>
|
||||
<View style={{ gap: 12, flex: 1 }}>
|
||||
<Image source={icons.musicLandLogo} style={{ alignSelf: "center" }} />
|
||||
<View style={{ flex: 1, gap: 24 }}>
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Image,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Image, Platform, ScrollView, Text, View } from "react-native";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { background, icons } from "../../assets";
|
||||
import SearchBar from "../../components/SearchBar";
|
||||
import useSearch from "../../hooks/useSearch";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { Palette, Style } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
@@ -21,6 +13,8 @@ import LikedMusic from "./components/LikedMusic";
|
||||
import LikedPlayback from "./components/LikedPlayback";
|
||||
import MyMusic from "./components/MyMusic";
|
||||
import MyPlaylist from "./components/MyPlaylist";
|
||||
import ResearchHeader from "./components/ResearchHeader";
|
||||
import SearchResultsList from "./components/SearchResultsList";
|
||||
const Library = () => {
|
||||
const {
|
||||
userProjects = [],
|
||||
@@ -51,18 +45,126 @@ const Library = () => {
|
||||
hasMyClips ||
|
||||
hasLikedClips;
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
selected,
|
||||
setSelected,
|
||||
toggleSelected,
|
||||
filteredProjects,
|
||||
projectsLoading,
|
||||
loadMoreProjects,
|
||||
filteredUsers,
|
||||
usersLoading,
|
||||
loadMoreUsers,
|
||||
} = useSearch();
|
||||
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
const searchWrapperRef = useRef(null);
|
||||
|
||||
const closeDropdown = useCallback(() => {
|
||||
setDropdownVisible(false);
|
||||
setSelected(null);
|
||||
setSearch("");
|
||||
}, [setSelected, setSearch]);
|
||||
|
||||
const handleFocus = () => {
|
||||
setDropdownVisible(true);
|
||||
};
|
||||
|
||||
const shouldShowResults = dropdownVisible;
|
||||
|
||||
const handleChangeText = (value) => {
|
||||
setSearch(value);
|
||||
setDropdownVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!dropdownVisible) return undefined;
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (searchWrapperRef.current?.contains(event.target)) return;
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("touchstart", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("touchstart", handleClickOutside);
|
||||
};
|
||||
}, [dropdownVisible, closeDropdown]);
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.libraryBG} headerType="NONE" width={"80%"}>
|
||||
<View style={{ gap: 17, paddingBottom: 20 }}>
|
||||
<View
|
||||
style={{ gap: 17, paddingBottom: 20, zIndex: dropdownVisible ? 40 : 1 }}
|
||||
>
|
||||
<Image source={icons.musicLandLogo} style={{ alignSelf: "center" }} />
|
||||
<Pressable onPress={() => navigate(Routes.Research)}>
|
||||
<View
|
||||
ref={searchWrapperRef}
|
||||
style={{
|
||||
position: "relative",
|
||||
alignSelf: "center",
|
||||
width: "65%",
|
||||
maxWidth: 520,
|
||||
minWidth: 360,
|
||||
zIndex: 40,
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
<SearchBar
|
||||
textInputProps={{
|
||||
editable: false,
|
||||
onPress: () => navigate(Routes.Research),
|
||||
value: search,
|
||||
onChangeText: handleChangeText,
|
||||
autoFocus: false,
|
||||
onFocus: handleFocus,
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
{shouldShowResults && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 60,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 50,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
gap: 14,
|
||||
borderRadius: 18,
|
||||
padding: 16,
|
||||
backgroundColor: Palette.ultraLightWhite,
|
||||
...Style.defaultBorder,
|
||||
width: "100%",
|
||||
zIndex: 50,
|
||||
elevation: 12,
|
||||
}}
|
||||
>
|
||||
<ResearchHeader
|
||||
onPress={toggleSelected}
|
||||
selected={selected}
|
||||
showSearchBar={false}
|
||||
/>
|
||||
<SearchResultsList
|
||||
selected={selected}
|
||||
filteredProjects={filteredProjects}
|
||||
projectsLoading={projectsLoading}
|
||||
loadMoreProjects={loadMoreProjects}
|
||||
filteredUsers={filteredUsers}
|
||||
usersLoading={usersLoading}
|
||||
loadMoreUsers={loadMoreUsers}
|
||||
onResultSelected={closeDropdown}
|
||||
style={{ flex: 0, maxHeight: 420 }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView
|
||||
|
||||
+27
-382
@@ -1,225 +1,25 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Image, Pressable, ScrollView, Text, View } from "react-native";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import { background, img } from "../../assets";
|
||||
import MoreMenu from "../../components/MoreMenu";
|
||||
import { usersRef, projectsRef } from "../../config/firebase";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import { useGlobal } from "reactn";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { background } from "../../assets";
|
||||
import useSearch from "../../hooks/useSearch";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { gutters, Palette, Style } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { size } from "../../styles/Style";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import MusicCard from "./components/MusicCard";
|
||||
import { gutters } from "../../styles";
|
||||
import ResearchHeader from "./components/ResearchHeader";
|
||||
import SearchResultsList from "./components/SearchResultsList";
|
||||
|
||||
const Research = () => {
|
||||
const [selected, setSelected] = useState(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const queryText = useMemo(() => search.trim(), [search]);
|
||||
const queryLower = useMemo(
|
||||
() => (search || "").toLowerCase().trim(),
|
||||
[search],
|
||||
);
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const [autoLoads, setAutoLoads] = useState(0);
|
||||
|
||||
const usersQueryRef = useMemo(() => {
|
||||
try {
|
||||
const lower = queryLower;
|
||||
if (lower) {
|
||||
return usersRef
|
||||
.orderBy("userNameLower")
|
||||
.startAt(lower)
|
||||
.endAt(lower + "\uf8ff");
|
||||
}
|
||||
return usersRef.orderBy("userNameLower");
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}, [queryLower]);
|
||||
|
||||
const projectsQueryRefLower = useMemo(() => {
|
||||
try {
|
||||
const lower = queryText ? queryText.toLowerCase() : "";
|
||||
if (lower) {
|
||||
return projectsRef
|
||||
.orderBy("titleLower")
|
||||
.startAt(lower)
|
||||
.endAt(lower + "\uf8ff");
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}, [queryText]);
|
||||
|
||||
const projectsQueryRefCase = useMemo(() => {
|
||||
try {
|
||||
const raw = queryText || "";
|
||||
if (raw) {
|
||||
return projectsRef
|
||||
.orderBy("title")
|
||||
.startAt(raw)
|
||||
.endAt(raw + "\uf8ff");
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}, [queryText]);
|
||||
|
||||
const {
|
||||
data: userResults = [],
|
||||
loading: usersLoading,
|
||||
loadMore: loadMoreUsers,
|
||||
} = useDataFromRef({
|
||||
ref: usersQueryRef,
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
condition: !!usersQueryRef,
|
||||
refreshArray: [queryLower],
|
||||
usePagination: true,
|
||||
batchSize: 5,
|
||||
});
|
||||
const filteredUsers = useMemo(() => {
|
||||
let arr = Array.isArray(userResults) ? userResults : [];
|
||||
// Client-side safety filter to guarantee match on pseudo
|
||||
if (queryLower) {
|
||||
arr = arr.filter((u) => {
|
||||
const nameLower = (u?.userNameLower || u?.userName || "").toLowerCase();
|
||||
return nameLower.includes(queryLower);
|
||||
});
|
||||
}
|
||||
const seen = new Set();
|
||||
const unique = [];
|
||||
for (const u of arr) {
|
||||
const id = u?.id;
|
||||
if (!id || seen.has(id)) continue;
|
||||
if (id === currentUID || u?.userID === currentUID) continue;
|
||||
seen.add(id);
|
||||
unique.push(u);
|
||||
}
|
||||
return unique;
|
||||
}, [JSON.stringify(userResults), queryLower, currentUID]);
|
||||
|
||||
const {
|
||||
data: projectResultsLower = [],
|
||||
loading: projectsLoadingLower,
|
||||
loadMore: loadMoreProjectsLower,
|
||||
} = useDataFromRef({
|
||||
ref: projectsQueryRefLower,
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
condition: !!projectsQueryRefLower,
|
||||
refreshArray: [queryText],
|
||||
usePagination: true,
|
||||
batchSize: 6,
|
||||
});
|
||||
const {
|
||||
data: projectResultsCase = [],
|
||||
loading: projectsLoadingCase,
|
||||
loadMore: loadMoreProjectsCase,
|
||||
} = useDataFromRef({
|
||||
ref: projectsQueryRefCase,
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
condition: !!projectsQueryRefCase,
|
||||
refreshArray: [queryText],
|
||||
usePagination: true,
|
||||
batchSize: 6,
|
||||
});
|
||||
const {
|
||||
data: projectResultsDefault = [],
|
||||
loading: projectsLoadingDefault,
|
||||
loadMore: loadMoreProjectsDefault,
|
||||
} = useDataFromRef({
|
||||
ref: !queryText ? projectsRef.orderBy("updatedAt", "desc") : null,
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
condition: !queryText,
|
||||
refreshArray: [queryText],
|
||||
usePagination: true,
|
||||
batchSize: 6,
|
||||
});
|
||||
|
||||
const projectsLoading = queryText
|
||||
? projectsLoadingLower || projectsLoadingCase
|
||||
: projectsLoadingDefault;
|
||||
const loadMoreProjects = () => {
|
||||
if (queryText) {
|
||||
loadMoreProjectsLower?.();
|
||||
loadMoreProjectsCase?.();
|
||||
} else {
|
||||
loadMoreProjectsDefault?.();
|
||||
}
|
||||
};
|
||||
const filteredProjects = useMemo(() => {
|
||||
const arr = queryText
|
||||
? [
|
||||
...(Array.isArray(projectResultsLower) ? projectResultsLower : []),
|
||||
...(Array.isArray(projectResultsCase) ? projectResultsCase : []),
|
||||
]
|
||||
: Array.isArray(projectResultsDefault)
|
||||
? projectResultsDefault
|
||||
: [];
|
||||
const seen = new Set();
|
||||
const unique = [];
|
||||
for (const p of arr) {
|
||||
const id = p?.id;
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
unique.push(p);
|
||||
}
|
||||
return unique;
|
||||
}, [
|
||||
queryText,
|
||||
JSON.stringify(projectResultsLower),
|
||||
JSON.stringify(projectResultsCase),
|
||||
JSON.stringify(projectResultsDefault),
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setAutoLoads(0);
|
||||
}, [queryText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filteredUsers.length < 5 && usersQueryRef && !usersLoading) {
|
||||
if (autoLoads < 3) {
|
||||
setAutoLoads((n) => n + 1);
|
||||
loadMoreUsers();
|
||||
}
|
||||
}
|
||||
}, [filteredUsers.length, usersLoading, usersQueryRef, autoLoads]);
|
||||
const [menuPosition, setMenuPosition] = useState(null);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState(null);
|
||||
|
||||
const onPressMenu = (item) => {
|
||||
if (selected === item) {
|
||||
setSelected(null);
|
||||
} else {
|
||||
setSelected(item);
|
||||
}
|
||||
};
|
||||
|
||||
const EmptyText = ({ text }) => (
|
||||
<View style={{ flex: 1, alignItems: "center", paddingVertical: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
search,
|
||||
setSearch,
|
||||
selected,
|
||||
toggleSelected,
|
||||
filteredProjects,
|
||||
projectsLoading,
|
||||
loadMoreProjects,
|
||||
filteredUsers,
|
||||
usersLoading,
|
||||
loadMoreUsers,
|
||||
} = useSearch();
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -232,175 +32,20 @@ const Research = () => {
|
||||
>
|
||||
<View style={{ flex: 1, gap: 10 }}>
|
||||
<ResearchHeader
|
||||
onPress={(item) => onPressMenu(item)}
|
||||
onPress={toggleSelected}
|
||||
selected={selected}
|
||||
searchValue={search}
|
||||
onChangeSearch={setSearch}
|
||||
/>
|
||||
<View style={{ flex: 1 }}>
|
||||
<ScrollView contentContainerStyle={{ paddingTop: 2 }}>
|
||||
{((!selected && (filteredProjects.length > 0 || projectsLoading)) ||
|
||||
selected === "Musiques") && (
|
||||
<View style={{ paddingHorizontal: 2 }}>
|
||||
<CreateLyricsHeader
|
||||
gradientProps={{
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: 0, y: 1 },
|
||||
}}
|
||||
>
|
||||
{filteredProjects.length > 0 ? (
|
||||
<View style={{ gap: 10 }}>
|
||||
{Array.isArray(filteredProjects) &&
|
||||
filteredProjects.slice(0, 6).map((p) => (
|
||||
<MusicCard
|
||||
key={p.id}
|
||||
title={p?.title || "Sans titre"}
|
||||
subtitle={"MusicLand"}
|
||||
imageUri={p?.coverUrl || null}
|
||||
projectId={p?.id}
|
||||
likedBy={p?.likedBy || []}
|
||||
onPress={() =>
|
||||
navigate(Routes.MusicDetails, { projectId: p.id })
|
||||
}
|
||||
onPressMore={(posTop) => {
|
||||
setSelectedProjectId(p.id);
|
||||
setMenuPosition(posTop);
|
||||
setShowMenu(
|
||||
(prev) =>
|
||||
!prev ||
|
||||
posTop?.top !== (menuPosition?.top ?? null),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{(filteredProjects.length >= 6 || projectsLoading) && (
|
||||
<Pressable
|
||||
onPress={loadMoreProjects}
|
||||
style={{ alignSelf: "center", marginTop: 6 }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
opacity: projectsLoading ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{projectsLoading ? "Chargement…" : "Charger plus"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
!projectsLoading && (
|
||||
<EmptyText text={"Aucune musique trouvée"} />
|
||||
)
|
||||
)}
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)}
|
||||
{((!selected && (filteredUsers.length > 0 || usersLoading)) ||
|
||||
selected === "Profils") && (
|
||||
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
|
||||
<CreateLyricsHeader
|
||||
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
|
||||
>
|
||||
{filteredUsers.length > 0 ? (
|
||||
<View style={{ gap: 8 }}>
|
||||
{Array.isArray(filteredUsers) &&
|
||||
filteredUsers.slice(0, 5).map((u) => (
|
||||
<Pressable
|
||||
style={{ gap: 14, ...Style.containerRow }}
|
||||
key={u.id}
|
||||
onPress={() =>
|
||||
navigate(Routes.SingerProfile, { userId: u.id })
|
||||
}
|
||||
>
|
||||
<ExpoImage
|
||||
source={
|
||||
u?.pictureUrl ||
|
||||
u?.profilePictureURL ||
|
||||
u?.photoURL
|
||||
? {
|
||||
uri:
|
||||
u?.pictureUrl ||
|
||||
u?.profilePictureURL ||
|
||||
u?.photoURL,
|
||||
}
|
||||
: img.profile
|
||||
}
|
||||
cachePolicy="memory-disk"
|
||||
priority="high"
|
||||
contentFit="cover"
|
||||
transition={100}
|
||||
style={{
|
||||
...size({ size: 60 }),
|
||||
borderRadius: 100,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{u?.userName || "Utilisateur"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
{(filteredUsers.length >= 5 || usersLoading) && (
|
||||
<Pressable
|
||||
onPress={loadMoreUsers}
|
||||
style={{ alignSelf: "center", marginTop: 6 }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
opacity: usersLoading ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{usersLoading ? "Chargement…" : "Charger plus"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
!usersLoading && <EmptyText text={"Aucun profil trouvé"} />
|
||||
)}
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)}
|
||||
{selected === "Playbacks" && (
|
||||
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
|
||||
<CreateLyricsHeader
|
||||
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
|
||||
>
|
||||
<EmptyText text={"Aucun playback trouvé"} />
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)}
|
||||
{selected === "Clips" && (
|
||||
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
|
||||
<CreateLyricsHeader
|
||||
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
|
||||
>
|
||||
<EmptyText text={"Aucun clip trouvé"} />
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)}
|
||||
<MoreMenu
|
||||
visible={showMenu}
|
||||
top={menuPosition?.top ?? 0}
|
||||
position={menuPosition}
|
||||
onClose={() => setShowMenu(false)}
|
||||
inPlaylist={false}
|
||||
projectId={selectedProjectId}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<SearchResultsList
|
||||
selected={selected}
|
||||
filteredProjects={filteredProjects}
|
||||
projectsLoading={projectsLoading}
|
||||
loadMoreProjects={loadMoreProjects}
|
||||
filteredUsers={filteredUsers}
|
||||
usersLoading={usersLoading}
|
||||
loadMoreUsers={loadMoreUsers}
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -11,23 +11,27 @@ const ResearchHeader = ({
|
||||
selected,
|
||||
searchValue = "",
|
||||
onChangeSearch = () => {},
|
||||
showSearchBar = true,
|
||||
autoFocusSearch = true,
|
||||
}) => {
|
||||
return (
|
||||
<View style={{ gap: 12 }}>
|
||||
<SearchBar
|
||||
textInputProps={{
|
||||
value: searchValue,
|
||||
onChangeText: onChangeSearch,
|
||||
autoFocus: true,
|
||||
}}
|
||||
/>
|
||||
{showSearchBar && (
|
||||
<SearchBar
|
||||
textInputProps={{
|
||||
value: searchValue,
|
||||
onChangeText: onChangeSearch,
|
||||
autoFocus: autoFocusSearch,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
...Style.containerRow,
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
{["Playbacks", "Clips", "Musiques", "Profils"].map((item, index) => (
|
||||
{["Musiques", "Playbacks", "Profils"].map((item, index) => (
|
||||
<Pressable key={index} onPress={() => onPress(item)}>
|
||||
<BorderGradient
|
||||
gradientProps={{
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import React, { useState } from "react";
|
||||
import { Pressable, ScrollView, Text, View } from "react-native";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import { img } from "../../../assets";
|
||||
import MoreMenu from "../../../components/MoreMenu";
|
||||
import { Routes } from "../../../navigation";
|
||||
import { navigate } from "../../../navigation/NavigationService";
|
||||
import { Palette, Style } from "../../../styles";
|
||||
import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
import { size } from "../../../styles/Style";
|
||||
import CreateLyricsHeader from "../../Writing/components/CreateLyricsHeader";
|
||||
import MusicCard from "./MusicCard";
|
||||
|
||||
const EmptyText = ({ text }) => (
|
||||
<View style={{ flex: 1, alignItems: "center", paddingVertical: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const SearchResultsList = ({
|
||||
selected,
|
||||
filteredProjects,
|
||||
projectsLoading,
|
||||
loadMoreProjects,
|
||||
filteredUsers,
|
||||
usersLoading,
|
||||
loadMoreUsers,
|
||||
onResultSelected,
|
||||
contentContainerStyle,
|
||||
style,
|
||||
}) => {
|
||||
const [menuPosition, setMenuPosition] = useState(null);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [selectedProjectId, setSelectedProjectId] = useState(null);
|
||||
|
||||
const handleProjectPress = (projectId) => {
|
||||
navigate(Routes.MusicDetails, { projectId });
|
||||
onResultSelected?.();
|
||||
};
|
||||
|
||||
const handleProfilePress = (userId) => {
|
||||
navigate(Routes.SingerProfile, { userId });
|
||||
onResultSelected?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[{ flex: 1 }, style]}>
|
||||
<ScrollView
|
||||
contentContainerStyle={{ paddingTop: 2, ...contentContainerStyle }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{((!selected && (filteredProjects.length > 0 || projectsLoading)) ||
|
||||
selected === "Musiques") && (
|
||||
<View style={{ paddingHorizontal: 2 }}>
|
||||
<CreateLyricsHeader
|
||||
gradientProps={{
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: 0, y: 1 },
|
||||
}}
|
||||
>
|
||||
{filteredProjects.length > 0 && (
|
||||
<View style={{ gap: 10 }}>
|
||||
{Array.isArray(filteredProjects) &&
|
||||
filteredProjects.slice(0, 6).map((project) => (
|
||||
<MusicCard
|
||||
key={project.id}
|
||||
title={project?.title || "Sans titre"}
|
||||
subtitle={"MusicLand"}
|
||||
imageUri={project?.coverUrl || null}
|
||||
projectId={project?.id}
|
||||
likedBy={project?.likedBy || []}
|
||||
onPress={() => handleProjectPress(project.id)}
|
||||
onPressMore={(positionTop) => {
|
||||
setSelectedProjectId(project.id);
|
||||
setMenuPosition(positionTop);
|
||||
setShowMenu(
|
||||
(previous) =>
|
||||
!previous ||
|
||||
positionTop?.top !== (menuPosition?.top ?? null),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{(filteredProjects.length >= 6 || projectsLoading) && (
|
||||
<Pressable
|
||||
onPress={loadMoreProjects}
|
||||
style={{ alignSelf: "center", marginTop: 6 }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
opacity: projectsLoading ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{projectsLoading ? "Chargement…" : "Charger plus"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{filteredProjects.length === 0 && !projectsLoading && (
|
||||
<EmptyText text={"Aucune musique trouvée"} />
|
||||
)}
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)}
|
||||
{((!selected && (filteredUsers.length > 0 || usersLoading)) ||
|
||||
selected === "Profils") && (
|
||||
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
|
||||
<CreateLyricsHeader
|
||||
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
|
||||
>
|
||||
{filteredUsers.length > 0 && (
|
||||
<View style={{ gap: 8 }}>
|
||||
{Array.isArray(filteredUsers) &&
|
||||
filteredUsers.slice(0, 5).map((user) => (
|
||||
<Pressable
|
||||
style={{ gap: 14, ...Style.containerRow }}
|
||||
key={user.id}
|
||||
onPress={() => handleProfilePress(user.id)}
|
||||
>
|
||||
<ExpoImage
|
||||
source={
|
||||
user?.pictureUrl ||
|
||||
user?.profilePictureURL ||
|
||||
user?.photoURL
|
||||
? {
|
||||
uri:
|
||||
user?.pictureUrl ||
|
||||
user?.profilePictureURL ||
|
||||
user?.photoURL,
|
||||
}
|
||||
: img.profile
|
||||
}
|
||||
cachePolicy="memory-disk"
|
||||
priority="high"
|
||||
contentFit="cover"
|
||||
transition={100}
|
||||
style={{
|
||||
...size({ size: 60 }),
|
||||
borderRadius: 100,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{user?.userName || "Utilisateur"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
{(filteredUsers.length >= 5 || usersLoading) && (
|
||||
<Pressable
|
||||
onPress={loadMoreUsers}
|
||||
style={{ alignSelf: "center", marginTop: 6 }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
opacity: usersLoading ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{usersLoading ? "Chargement…" : "Charger plus"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{filteredUsers.length === 0 && !usersLoading && (
|
||||
<EmptyText text={"Aucun profil trouvé"} />
|
||||
)}
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)}
|
||||
{selected === "Playbacks" && (
|
||||
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
|
||||
<CreateLyricsHeader
|
||||
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
|
||||
>
|
||||
<EmptyText text={"Aucun playback trouvé"} />
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)}
|
||||
{selected === "Clips" && (
|
||||
<View style={{ paddingHorizontal: 2, paddingTop: 10 }}>
|
||||
<CreateLyricsHeader
|
||||
gradientProps={{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 } }}
|
||||
>
|
||||
<EmptyText text={"Aucun clip trouvé"} />
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
<MoreMenu
|
||||
visible={showMenu}
|
||||
top={menuPosition?.top ?? 0}
|
||||
position={menuPosition}
|
||||
onClose={() => setShowMenu(false)}
|
||||
inPlaylist={false}
|
||||
projectId={selectedProjectId}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResultsList;
|
||||
@@ -65,7 +65,7 @@ const Profile = () => {
|
||||
setFollowers(
|
||||
Array.isArray(currentUserData?.followedBy)
|
||||
? currentUserData.followedBy.length
|
||||
: 0
|
||||
: 0,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -139,7 +139,8 @@ const Profile = () => {
|
||||
setSelectedProjectId(item.id);
|
||||
setMenuPosition(posTop);
|
||||
setShowMenu(
|
||||
(prev) => !prev || posTop?.top !== (menuPosition?.top ?? null)
|
||||
(prev) =>
|
||||
!prev || posTop?.top !== (menuPosition?.top ?? null),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -231,7 +232,7 @@ const Profile = () => {
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.profileBG}
|
||||
backgroundImg={isWeb ? background.profileBgWeb : background.profileBG}
|
||||
headerType="NAVIGATE"
|
||||
hideBackButton={params?.noBack}
|
||||
contentContainerStyle={{
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const normalize = (s = "") =>
|
||||
s
|
||||
.normalize("NFD")
|
||||
.replace(/\p{Diacritic}/gu, "")
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
Reference in New Issue
Block a user