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,
|
playbackBG,
|
||||||
playbackBG2,
|
playbackBG2,
|
||||||
libraryBG,
|
libraryBG,
|
||||||
|
libraryBgWeb: require("./UI/libraryBgWeb.png"),
|
||||||
libraryBG2,
|
libraryBG2,
|
||||||
profileBG,
|
profileBG,
|
||||||
|
profileBgWeb: require("./UI/profileBgWeb.png"),
|
||||||
hitParadeBG,
|
hitParadeBG,
|
||||||
homeBG,
|
homeBG,
|
||||||
homeBGWeb: require("./UI/homeBGWeb.png"),
|
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 React from "react";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import { icons } from "../assets";
|
import { icons } from "../assets";
|
||||||
@@ -20,9 +20,6 @@ const SearchBar = ({
|
|||||||
gap: 10,
|
gap: 10,
|
||||||
...Style.containerRow,
|
...Style.containerRow,
|
||||||
}}
|
}}
|
||||||
// experimentalBlurMethod={
|
|
||||||
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
|
||||||
// }
|
|
||||||
>
|
>
|
||||||
<Image source={icons.search} />
|
<Image source={icons.search} />
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -23,14 +23,17 @@ export default function useDataFromRef({
|
|||||||
|
|
||||||
format = null,
|
format = null,
|
||||||
onUpdate = () => null,
|
onUpdate = () => null,
|
||||||
|
onError = null,
|
||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [endReached, setEndReached] = useState(false);
|
const [endReached, setEndReached] = useState(false);
|
||||||
const [data, setData] = useState(initialState);
|
const [data, setData] = useState(initialState);
|
||||||
const [lastVisible, setLastVisible] = useState(null);
|
const [lastVisible, setLastVisible] = useState(null);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (condition) {
|
if (condition) {
|
||||||
|
setError(null);
|
||||||
if (listener && !usePagination) {
|
if (listener && !usePagination) {
|
||||||
const subData = getListenerData();
|
const subData = getListenerData();
|
||||||
return () => subData?.();
|
return () => subData?.();
|
||||||
@@ -57,12 +60,14 @@ export default function useDataFromRef({
|
|||||||
}
|
}
|
||||||
setEndReached(false);
|
setEndReached(false);
|
||||||
setLastVisible(null);
|
setLastVisible(null);
|
||||||
|
setError(null);
|
||||||
setData(initialState);
|
setData(initialState);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getData = async ({ paginate = false } = {}) => {
|
const getData = async ({ paginate = false } = {}) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
let newData = simpleRef ? null : [];
|
let newData = simpleRef ? null : [];
|
||||||
let initialDoc = null;
|
let initialDoc = null;
|
||||||
|
|
||||||
@@ -115,6 +120,10 @@ export default function useDataFromRef({
|
|||||||
} else {
|
} else {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
setError(e);
|
||||||
|
if (onError) {
|
||||||
|
onError(e);
|
||||||
|
}
|
||||||
await updateData([]);
|
await updateData([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -124,6 +133,7 @@ export default function useDataFromRef({
|
|||||||
const getListenerData = () => {
|
const getListenerData = () => {
|
||||||
return ref?.onSnapshot(
|
return ref?.onSnapshot(
|
||||||
async (dataSnap) => {
|
async (dataSnap) => {
|
||||||
|
setError(null);
|
||||||
let newData;
|
let newData;
|
||||||
|
|
||||||
if (simpleRef) {
|
if (simpleRef) {
|
||||||
@@ -145,6 +155,10 @@ export default function useDataFromRef({
|
|||||||
} else {
|
} else {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
setError(e);
|
||||||
|
if (onError) {
|
||||||
|
onError(e);
|
||||||
|
}
|
||||||
await updateData([]);
|
await updateData([]);
|
||||||
setLoading(false);
|
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";
|
const isWeb = Platform.OS === "web";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page backgroundImg={background.hitParadeBG} headerType="NONE">
|
<Page
|
||||||
|
backgroundImg={isWeb ? background.libraryBgWeb : background.hitParadeBG}
|
||||||
|
headerType="NONE"
|
||||||
|
>
|
||||||
<View style={{ gap: 12, flex: 1 }}>
|
<View style={{ gap: 12, flex: 1 }}>
|
||||||
<Image source={icons.musicLandLogo} style={{ alignSelf: "center" }} />
|
<Image source={icons.musicLandLogo} style={{ alignSelf: "center" }} />
|
||||||
<View style={{ flex: 1, gap: 24 }}>
|
<View style={{ flex: 1, gap: 24 }}>
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
import React from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import { Image, Platform, ScrollView, Text, View } from "react-native";
|
||||||
Image,
|
|
||||||
Platform,
|
|
||||||
Pressable,
|
|
||||||
ScrollView,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
} from "react-native";
|
|
||||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||||
import { background, icons } from "../../assets";
|
import { background, icons } from "../../assets";
|
||||||
import SearchBar from "../../components/SearchBar";
|
import SearchBar from "../../components/SearchBar";
|
||||||
|
import useSearch from "../../hooks/useSearch";
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { Routes } from "../../navigation";
|
|
||||||
import { navigate } from "../../navigation/NavigationService";
|
|
||||||
import { useUser } from "../../providers/UserDataProvider";
|
import { useUser } from "../../providers/UserDataProvider";
|
||||||
import { Palette, Style } from "../../styles";
|
import { Palette, Style } from "../../styles";
|
||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
@@ -21,6 +13,8 @@ import LikedMusic from "./components/LikedMusic";
|
|||||||
import LikedPlayback from "./components/LikedPlayback";
|
import LikedPlayback from "./components/LikedPlayback";
|
||||||
import MyMusic from "./components/MyMusic";
|
import MyMusic from "./components/MyMusic";
|
||||||
import MyPlaylist from "./components/MyPlaylist";
|
import MyPlaylist from "./components/MyPlaylist";
|
||||||
|
import ResearchHeader from "./components/ResearchHeader";
|
||||||
|
import SearchResultsList from "./components/SearchResultsList";
|
||||||
const Library = () => {
|
const Library = () => {
|
||||||
const {
|
const {
|
||||||
userProjects = [],
|
userProjects = [],
|
||||||
@@ -51,18 +45,126 @@ const Library = () => {
|
|||||||
hasMyClips ||
|
hasMyClips ||
|
||||||
hasLikedClips;
|
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 (
|
return (
|
||||||
<Page backgroundImg={background.libraryBG} headerType="NONE" width={"80%"}>
|
<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" }} />
|
<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
|
<SearchBar
|
||||||
textInputProps={{
|
textInputProps={{
|
||||||
editable: false,
|
value: search,
|
||||||
onPress: () => navigate(Routes.Research),
|
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>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|||||||
+26
-381
@@ -1,225 +1,25 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React from "react";
|
||||||
import { Image, Pressable, ScrollView, Text, View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { Image as ExpoImage } from "expo-image";
|
import { background } from "../../assets";
|
||||||
import { background, img } from "../../assets";
|
import useSearch from "../../hooks/useSearch";
|
||||||
import MoreMenu from "../../components/MoreMenu";
|
|
||||||
import { usersRef, projectsRef } from "../../config/firebase";
|
|
||||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
|
||||||
import { useGlobal } from "reactn";
|
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { Routes } from "../../navigation";
|
import { gutters } from "../../styles";
|
||||||
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 ResearchHeader from "./components/ResearchHeader";
|
import ResearchHeader from "./components/ResearchHeader";
|
||||||
|
import SearchResultsList from "./components/SearchResultsList";
|
||||||
|
|
||||||
const Research = () => {
|
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 {
|
const {
|
||||||
data: userResults = [],
|
search,
|
||||||
loading: usersLoading,
|
setSearch,
|
||||||
loadMore: loadMoreUsers,
|
selected,
|
||||||
} = useDataFromRef({
|
toggleSelected,
|
||||||
ref: usersQueryRef,
|
filteredProjects,
|
||||||
simpleRef: false,
|
projectsLoading,
|
||||||
listener: false,
|
loadMoreProjects,
|
||||||
condition: !!usersQueryRef,
|
filteredUsers,
|
||||||
refreshArray: [queryLower],
|
usersLoading,
|
||||||
usePagination: true,
|
loadMoreUsers,
|
||||||
batchSize: 5,
|
} = useSearch();
|
||||||
});
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
@@ -232,175 +32,20 @@ const Research = () => {
|
|||||||
>
|
>
|
||||||
<View style={{ flex: 1, gap: 10 }}>
|
<View style={{ flex: 1, gap: 10 }}>
|
||||||
<ResearchHeader
|
<ResearchHeader
|
||||||
onPress={(item) => onPressMenu(item)}
|
onPress={toggleSelected}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
onChangeSearch={setSearch}
|
onChangeSearch={setSearch}
|
||||||
/>
|
/>
|
||||||
<View style={{ flex: 1 }}>
|
<SearchResultsList
|
||||||
<ScrollView contentContainerStyle={{ paddingTop: 2 }}>
|
selected={selected}
|
||||||
{((!selected && (filteredProjects.length > 0 || projectsLoading)) ||
|
filteredProjects={filteredProjects}
|
||||||
selected === "Musiques") && (
|
projectsLoading={projectsLoading}
|
||||||
<View style={{ paddingHorizontal: 2 }}>
|
loadMoreProjects={loadMoreProjects}
|
||||||
<CreateLyricsHeader
|
filteredUsers={filteredUsers}
|
||||||
gradientProps={{
|
usersLoading={usersLoading}
|
||||||
start: { x: 0, y: 0 },
|
loadMoreUsers={loadMoreUsers}
|
||||||
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>
|
|
||||||
</View>
|
</View>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,23 +11,27 @@ const ResearchHeader = ({
|
|||||||
selected,
|
selected,
|
||||||
searchValue = "",
|
searchValue = "",
|
||||||
onChangeSearch = () => {},
|
onChangeSearch = () => {},
|
||||||
|
showSearchBar = true,
|
||||||
|
autoFocusSearch = true,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<View style={{ gap: 12 }}>
|
<View style={{ gap: 12 }}>
|
||||||
|
{showSearchBar && (
|
||||||
<SearchBar
|
<SearchBar
|
||||||
textInputProps={{
|
textInputProps={{
|
||||||
value: searchValue,
|
value: searchValue,
|
||||||
onChangeText: onChangeSearch,
|
onChangeText: onChangeSearch,
|
||||||
autoFocus: true,
|
autoFocus: autoFocusSearch,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
...Style.containerRow,
|
...Style.containerRow,
|
||||||
gap: 6,
|
gap: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{["Playbacks", "Clips", "Musiques", "Profils"].map((item, index) => (
|
{["Musiques", "Playbacks", "Profils"].map((item, index) => (
|
||||||
<Pressable key={index} onPress={() => onPress(item)}>
|
<Pressable key={index} onPress={() => onPress(item)}>
|
||||||
<BorderGradient
|
<BorderGradient
|
||||||
gradientProps={{
|
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(
|
setFollowers(
|
||||||
Array.isArray(currentUserData?.followedBy)
|
Array.isArray(currentUserData?.followedBy)
|
||||||
? currentUserData.followedBy.length
|
? currentUserData.followedBy.length
|
||||||
: 0
|
: 0,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,8 @@ const Profile = () => {
|
|||||||
setSelectedProjectId(item.id);
|
setSelectedProjectId(item.id);
|
||||||
setMenuPosition(posTop);
|
setMenuPosition(posTop);
|
||||||
setShowMenu(
|
setShowMenu(
|
||||||
(prev) => !prev || posTop?.top !== (menuPosition?.top ?? null)
|
(prev) =>
|
||||||
|
!prev || posTop?.top !== (menuPosition?.top ?? null),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -231,7 +232,7 @@ const Profile = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
backgroundImg={background.profileBG}
|
backgroundImg={isWeb ? background.profileBgWeb : background.profileBG}
|
||||||
headerType="NAVIGATE"
|
headerType="NAVIGATE"
|
||||||
hideBackButton={params?.noBack}
|
hideBackButton={params?.noBack}
|
||||||
contentContainerStyle={{
|
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