search on web

This commit is contained in:
Thomas Demirdjian
2025-10-02 17:53:17 +02:00
parent ea421d8eff
commit 8846ca05ff
14 changed files with 656 additions and 415 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

+2
View File
@@ -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
View File
@@ -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
+15 -1
View File
@@ -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 };
} }
+12
View File
@@ -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;
}
+232
View File
@@ -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;
+4 -1
View File
@@ -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 }}>
+118 -16
View File
@@ -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
+27 -382
View File
@@ -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 }}>
<SearchBar {showSearchBar && (
textInputProps={{ <SearchBar
value: searchValue, textInputProps={{
onChangeText: onChangeSearch, value: searchValue,
autoFocus: true, onChangeText: onChangeSearch,
}} 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;
+4 -3
View File
@@ -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={{
+7
View File
@@ -0,0 +1,7 @@
export const normalize = (s = "") =>
s
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "")
.toLowerCase()
.replace(/\s+/g, " ")
.trim();