app layout

This commit is contained in:
2025-09-30 09:58:20 +02:00
parent dd8e8049c4
commit 3dcf5453ca
5 changed files with 153 additions and 146 deletions
+5 -5
View File
@@ -1,11 +1,11 @@
module.exports = { module.exports = {
extends: ['expo', 'prettier'], extends: ["expo", "prettier"],
plugins: ['prettier'], plugins: ["prettier"],
rules: { rules: {
'prettier/prettier': [ "prettier/prettier": [
'error', "error",
{ {
singleQuote: true, singleQuote: false,
}, },
], ],
}, },
+28 -27
View File
@@ -1,52 +1,51 @@
import { View, Image } from 'react-native'; import { Image, View } from "react-native";
import { useGlobal } from 'reactn'; import { useGlobal } from "reactn";
import { Motion } from '@legendapp/motion';
import useLayoutType from '../hooks/useLayoutType'; import useLayoutType from "../hooks/useLayoutType";
import Home from '../screens/Home'; import { background as backgrounds } from "../assets";
import { Style } from "../styles";
import { Palette, Style } from '../styles';
import { background as backgrounds } from '../assets';
export default ({ currentUID = null, children }) => { export default ({ currentUID = null, children }) => {
const { isWeb = false } = useLayoutType(); const { isWeb = false } = useLayoutType();
// Web: full-screen background with a centered content area (30% width) // Web: full-screen background with a centered content area (30% width)
const [webBackgroundImg] = useGlobal('webBackgroundImg'); const [webBackgroundImg] = useGlobal("webBackgroundImg");
if (isWeb) { if (isWeb) {
return ( return (
<View <View
style={{ style={{
flex: 1, flex: 1,
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
minHeight: '100svh', minHeight: "100svh",
position: 'relative', position: "relative",
}}> }}
>
{/* Full-viewport background for web */} {/* Full-viewport background for web */}
<Image <Image
source={webBackgroundImg || backgrounds.homeBG} source={webBackgroundImg || backgrounds.homeBG}
resizeMode='cover' resizeMode="cover"
style={{ style={{
position: 'absolute', position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
width: '100%', width: "100%",
height: '100%', height: "100%",
}} }}
/> />
{/* Centered content column (phone-like width) */} {/* Centered content column (phone-like width) */}
<View <View
style={{ style={{
width: '30%', width: isWeb ? "60%" : "30%",
minWidth: 360, minWidth: 360,
maxWidth: 520, maxWidth: isWeb ? 800 : 520,
height: '100svh', height: "100svh",
backgroundColor: 'transparent', backgroundColor: "transparent",
}}> }}
>
<View style={{ flex: 1 }}>{children}</View> <View style={{ flex: 1 }}>{children}</View>
</View> </View>
</View> </View>
@@ -59,17 +58,19 @@ export default ({ currentUID = null, children }) => {
<View <View
style={{ style={{
flex: 1, flex: 1,
flexDirection: 'row-reverse', flexDirection: "row-reverse",
}}> }}
>
<View <View
style={{ style={{
flex: 2, flex: 2,
position: 'relative', position: "relative",
...Style.defaultBorder, ...Style.defaultBorder,
borderTopWidth: 0, borderTopWidth: 0,
borderBottomWidth: 0, borderBottomWidth: 0,
...sharedPanelStyle, ...sharedPanelStyle,
}}> }}
>
{children} {children}
</View> </View>
</View> </View>
+27 -27
View File
@@ -1,21 +1,19 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import React, { useState, useEffect } from 'reactn'; import { Image, View } from "react-native";
import { setGlobal } from 'reactn'; import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { Image, View } from 'react-native'; import { SafeAreaView } from "react-native-safe-area-context";
import { SafeAreaView } from 'react-native-safe-area-context'; import React, { setGlobal, useEffect, useState } from "reactn";
import { Motion } from '@legendapp/motion';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { responsiveHeight } from '../actions/responsiveSizes.js'; import { responsiveHeight } from "../actions/responsiveSizes.js";
import { Fonts, gutters } from '../styles'; import { gutters } from "../styles";
import BaseHeader from '../components/BaseHeader'; import BaseHeader from "../components/BaseHeader";
import NavigateHeader from '../components/NavigateHeader'; import NavigateHeader from "../components/NavigateHeader";
import { isDesktop, isWeb } from '../hooks/useLayoutType.js'; import { background } from "../assets/index.js";
import { useUserData } from '../providers/UserDataProvider.js'; import { isDesktop, isWeb } from "../hooks/useLayoutType.js";
import { background } from '../assets/index.js'; import { useUserData } from "../providers/UserDataProvider.js";
export default ({ export default ({
children, children,
@@ -23,11 +21,11 @@ export default ({
topStickyContent = null, topStickyContent = null,
bottomStickyContent = null, bottomStickyContent = null,
title = '', title = "",
rightComponent = null, rightComponent = null,
containerType = 'SAFE_AREA_VIEW', containerType = "SAFE_AREA_VIEW",
headerType = 'BASE', headerType = "BASE",
containerStyle = {}, containerStyle = {},
headerStyle = {}, headerStyle = {},
@@ -45,7 +43,7 @@ export default ({
const [scrollPosition, setScrollPosition] = useState(0); const [scrollPosition, setScrollPosition] = useState(0);
const PageContainer = const PageContainer =
containerType === 'SAFE_AREA_VIEW' ? SafeAreaView : View; containerType === "SAFE_AREA_VIEW" ? SafeAreaView : View;
const ContentContainer = scrollEnabled ? KeyboardAwareScrollView : View; const ContentContainer = scrollEnabled ? KeyboardAwareScrollView : View;
@@ -68,24 +66,25 @@ export default ({
{!isWeb && ( {!isWeb && (
<Image <Image
source={backgroundImg} source={backgroundImg}
style={{ width: '100%', height: '100%', position: 'absolute' }} style={{ width: "100%", height: "100%", position: "absolute" }}
/> />
)} )}
<PageContainer <PageContainer
{...(containerType === 'SAFE_AREA_VIEW' ? { edges: ['top'] } : {})} {...(containerType === "SAFE_AREA_VIEW" ? { edges: ["top"] } : {})}
style={{ style={{
height: '100%', height: "100%",
paddingTop: isWeb ? gutters / 2 : 0, paddingTop: isWeb ? gutters / 2 : 0,
padding: gutters, padding: gutters,
paddingBottom: 0, paddingBottom: 0,
...(isWeb ? { maxHeight: '100vh', width: '100%' } : {}), ...(isWeb ? { maxHeight: "100vh", width: "100%" } : {}),
...(!currentUID && isDesktop ...(!currentUID && isDesktop
? { maxWidth: 500, alignSelf: 'center' } ? { maxWidth: 500, alignSelf: "center" }
: {}), : {}),
...containerStyle, ...containerStyle,
}}> }}
{headerType !== 'NONE' ? ( >
headerType === 'BASE' ? ( {headerType !== "NONE" ? (
headerType === "BASE" ? (
<BaseHeader containerStyle={{ ...headerStyle }} /> <BaseHeader containerStyle={{ ...headerStyle }} />
) : ( ) : (
<NavigateHeader <NavigateHeader
@@ -109,9 +108,10 @@ export default ({
scrollEventThrottle: 80, scrollEventThrottle: 80,
showsVerticalScrollIndicator: false, showsVerticalScrollIndicator: false,
contentContainerStyle: { paddingBottom: responsiveHeight(55) }, contentContainerStyle: { paddingBottom: responsiveHeight(55) },
keyboardDismissMode: 'on-drag', keyboardDismissMode: "on-drag",
} }
: {})}> : {})}
>
{children} {children}
</ContentContainer> </ContentContainer>
</PageContainer> </PageContainer>
+2 -5
View File
@@ -23,9 +23,6 @@ const HitParade = () => {
condition: true, condition: true,
}); });
const { data: topPlaybacks } = useDataFromRef({ const { data: topPlaybacks } = useDataFromRef({
// Firestore n'autorise pas 2 inégalités (>, <, >=, <=, !=) sur des champs différents.
// On ne garde qu'une inégalité côté requête (views > 0) et on filtre ensuite côté client.
// On surdimensionne le limit pour s'assurer d'obtenir 10 playbacks après filtrage.
ref: projectsRef.where("views", ">", 0).orderBy("views", "desc").limit(50), ref: projectsRef.where("views", ">", 0).orderBy("views", "desc").limit(50),
format: (docs) => docs.filter((d) => d?.playbackUrl != null).slice(0, 10), format: (docs) => docs.filter((d) => d?.playbackUrl != null).slice(0, 10),
simpleRef: false, simpleRef: false,
@@ -76,7 +73,7 @@ const HitParade = () => {
style={{ style={{
...Style.containerRow, ...Style.containerRow,
justifyContent: "space-between", justifyContent: "space-between",
gap: "10%", // gap: "5%",
width: "auto", width: "auto",
}} }}
> >
@@ -88,7 +85,7 @@ const HitParade = () => {
onPress={() => setSelected(item)} onPress={() => setSelected(item)}
tint={selected === item ? "light" : "dark"} tint={selected === item ? "light" : "dark"}
containerStyle={{ containerStyle={{
width: "50%", width: "47%",
}} }}
titleStyle={{ titleStyle={{
fontSize: 16, fontSize: 16,
+91 -82
View File
@@ -1,37 +1,37 @@
import { useRoute } from '@react-navigation/native'; import { useRoute } from "@react-navigation/native";
import { BlurView } from 'expo-blur'; import { BlurView } from "expo-blur";
import { Image as ExpoImage } from 'expo-image'; import { Image as ExpoImage } from "expo-image";
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from "react";
import { import {
FlatList, FlatList,
Image, Image,
Platform,
Pressable, Pressable,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
Text, Text,
View, View,
} from 'react-native'; } from "react-native";
import { SheetManager } from 'react-native-actions-sheet'; import { SheetManager } from "react-native-actions-sheet";
import { responsiveHeight } from 'react-native-responsive-dimensions'; import { responsiveHeight } from "react-native-responsive-dimensions";
import { useGlobal } from 'reactn'; import { useGlobal } from "reactn";
import { background, icons, img } from '../../assets'; import { background, icons, img } from "../../assets";
import BorderGradient from '../../components/BorderGradient/BorderGradient'; import BorderGradient from "../../components/BorderGradient/BorderGradient";
import MoreMenu from '../../components/MoreMenu'; import MoreMenu from "../../components/MoreMenu";
import { projectsRef, usersRef } from '../../config/firebase'; import { projectsRef, usersRef } from "../../config/firebase";
import useDataFromRef from '../../hooks/useDataFromRef'; import useDataFromRef from "../../hooks/useDataFromRef";
import Page from '../../layouts/Page'; import useLayoutType from "../../hooks/useLayoutType";
import { Routes } from '../../navigation'; import Page from "../../layouts/Page";
import { navigate, push } from '../../navigation/NavigationService'; import { Routes } from "../../navigation";
import { useUser } from '../../providers/UserDataProvider'; import { navigate, push } from "../../navigation/NavigationService";
import { Palette } from '../../styles'; import { useUser } from "../../providers/UserDataProvider";
import { FONT_FAMILY } from '../../styles/Fonts'; import { Palette } from "../../styles";
import Style, { gutters, size } from '../../styles/Style'; import { FONT_FAMILY } from "../../styles/Fonts";
import MusicCard from '../Library/components/MusicCard'; import Style, { gutters, size } from "../../styles/Style";
import MusicCard from "../Library/components/MusicCard";
const Profile = () => { const Profile = () => {
const { params } = useRoute(); const { params } = useRoute();
const [selected, setSelected] = useState('Chansons'); const [selected, setSelected] = useState("Chansons");
const { isWeb } = useLayoutType();
const { const {
userProjects: selfProjects, userProjects: selfProjects,
userPlaybacks: selfPlaybacks, userPlaybacks: selfPlaybacks,
@@ -41,10 +41,10 @@ const Profile = () => {
followUser, followUser,
unfollowUser, unfollowUser,
} = useUser(); } = useUser();
const [currentUID] = useGlobal('currentUID'); const [currentUID] = useGlobal("currentUID");
const targetUserId = params?.userId || currentUID || null; const targetUserId = params?.userId || currentUID || null;
const isSelf = !!currentUID && targetUserId === currentUID; const isSelf = !!currentUID && targetUserId === currentUID;
const [, setTooltip] = useGlobal('_tooltip'); const [, setTooltip] = useGlobal("_tooltip");
const [userData, setUserData] = useState(null); const [userData, setUserData] = useState(null);
const [isFollowing, setIsFollowing] = useState(false); const [isFollowing, setIsFollowing] = useState(false);
const [followers, setFollowers] = useState(0); const [followers, setFollowers] = useState(0);
@@ -82,7 +82,7 @@ const Profile = () => {
const followingQueryRef = useMemo(() => { const followingQueryRef = useMemo(() => {
try { try {
return targetUserId return targetUserId
? usersRef.where('followedBy', 'array-contains', targetUserId) ? usersRef.where("followedBy", "array-contains", targetUserId)
: null; : null;
} catch (e) { } catch (e) {
return null; return null;
@@ -124,10 +124,10 @@ const Profile = () => {
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
renderItem={({ item }) => ( renderItem={({ item }) => (
<MusicCard <MusicCard
title={item?.title || 'Sans titre'} title={item?.title || "Sans titre"}
subtitle={ subtitle={
(isSelf ? currentUserData?.userName : userData?.userName) || (isSelf ? currentUserData?.userName : userData?.userName) ||
'MusicLand' "MusicLand"
} }
imageUri={item?.coverUrl || null} imageUri={item?.coverUrl || null}
projectId={item?.id} projectId={item?.id}
@@ -156,25 +156,25 @@ const Profile = () => {
isSelf isSelf
? [ ? [
{ {
label: 'Supprimer', label: "Supprimer",
onPress: () => onPress: () =>
SheetManager.show('Delete', { SheetManager.show("Delete", {
payload: { payload: {
title: 'Supprimer le projet', title: "Supprimer le projet",
message: message:
'Cette action supprimera définitivement ce projet.', "Cette action supprimera définitivement ce projet.",
onConfirm: async () => { onConfirm: async () => {
try { try {
if (!selectedProjectId) return; if (!selectedProjectId) return;
await projectsRef.doc(selectedProjectId).delete(); await projectsRef.doc(selectedProjectId).delete();
setTooltip({ setTooltip({
type: 'success', type: "success",
text: 'Projet supprimé', text: "Projet supprimé",
}); });
} catch (e) { } catch (e) {
setTooltip({ setTooltip({
type: 'error', type: "error",
text: e?.message || 'Suppression impossible', text: e?.message || "Suppression impossible",
}); });
} }
}, },
@@ -196,7 +196,8 @@ const Profile = () => {
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
opacity: 0.8, opacity: 0.8,
}}> }}
>
{text} {text}
</Text> </Text>
</View> </View>
@@ -207,8 +208,8 @@ const Profile = () => {
try { try {
if (!targetUserId || isSelf) return null; if (!targetUserId || isSelf) return null;
return projectsRef return projectsRef
.where('userId', '==', targetUserId) .where("userId", "==", targetUserId)
.orderBy('updatedAt', 'desc'); .orderBy("updatedAt", "desc");
} catch (e) { } catch (e) {
return null; return null;
} }
@@ -228,23 +229,24 @@ const Profile = () => {
return ( return (
<Page <Page
backgroundImg={background.profileBG} backgroundImg={background.profileBG}
headerType='NAVIGATE' headerType="NAVIGATE"
hideBackButton={params?.noBack} hideBackButton={params?.noBack}
contentContainerStyle={{ contentContainerStyle={{
paddingBottom: gutters * 2, paddingBottom: gutters * 2,
}} }}
containerStyle={{ containerStyle={{
backgroundColor: '#0000004D', backgroundColor: isWeb ? "transparent" : "#0000004D",
}} }}
rightComponent={() => rightComponent={() =>
isSelf ? ( isSelf ? (
<Pressable onPress={() => SheetManager.show('ProfileSettings')}> <Pressable onPress={() => SheetManager.show("ProfileSettings")}>
<Image source={icons.more} style={{ ...size({ size: 24 }) }} /> <Image source={icons.more} style={{ ...size({ size: 24 }) }} />
</Pressable> </Pressable>
) : null ) : null
}> }
>
<View style={{ flex: 1, marginTop: responsiveHeight(2), gap: 20 }}> <View style={{ flex: 1, marginTop: responsiveHeight(2), gap: 20 }}>
<View style={{ borderRadius: 20, overflow: 'hidden' }}> <View style={{ borderRadius: 20, overflow: "hidden" }}>
<BlurView <BlurView
intensity={20} intensity={20}
style={{ style={{
@@ -256,7 +258,7 @@ const Profile = () => {
// Platform.OS !== "ios" ? "dimezisBlurView" : "none" // Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// } // }
> >
<View style={{ alignItems: 'center' }}> <View style={{ alignItems: "center" }}>
<ExpoImage <ExpoImage
source={ source={
( (
@@ -271,9 +273,9 @@ const Profile = () => {
} }
: img.profile : img.profile
} }
cachePolicy='memory-disk' cachePolicy="memory-disk"
priority='high' priority="high"
contentFit='cover' contentFit="cover"
transition={100} transition={100}
style={{ style={{
...size({ size: 108 }), ...size({ size: 108 }),
@@ -285,13 +287,14 @@ const Profile = () => {
fontSize: 22, fontSize: 22,
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
}}> }}
>
{(isSelf ? currentUserData?.userName : userData?.userName) || {(isSelf ? currentUserData?.userName : userData?.userName) ||
'Profil'} "Profil"}
</Text> </Text>
</View> </View>
<View style={{ ...Style.containerRow, gap: 10, marginTop: 10 }}> <View style={{ ...Style.containerRow, gap: 10, marginTop: 10 }}>
<View style={{ flex: 1, alignItems: 'center' }}> <View style={{ flex: 1, alignItems: "center" }}>
<Text style={styles.value}> <Text style={styles.value}>
{Array.isArray(displayedProjects) {Array.isArray(displayedProjects)
? displayedProjects.length ? displayedProjects.length
@@ -300,13 +303,14 @@ const Profile = () => {
<Text style={styles.label}>projets</Text> <Text style={styles.label}>projets</Text>
</View> </View>
<Pressable <Pressable
style={{ flex: 1, alignItems: 'center' }} style={{ flex: 1, alignItems: "center" }}
onPress={() => onPress={() =>
push(Routes.Follows, { push(Routes.Follows, {
selected: 'Abonnés', selected: "Abonnés",
...(isSelf ? {} : { userId: targetUserId }), ...(isSelf ? {} : { userId: targetUserId }),
}) })
}> }
>
<Text style={styles.value}> <Text style={styles.value}>
{isSelf {isSelf
? currentUserData?.followedBy?.length || 0 ? currentUserData?.followedBy?.length || 0
@@ -315,13 +319,14 @@ const Profile = () => {
<Text style={styles.label}>abonnés</Text> <Text style={styles.label}>abonnés</Text>
</Pressable> </Pressable>
<Pressable <Pressable
style={{ flex: 1, alignItems: 'center' }} style={{ flex: 1, alignItems: "center" }}
onPress={() => onPress={() =>
push(Routes.Follows, { push(Routes.Follows, {
selected: 'Abonnements', selected: "Abonnements",
...(isSelf ? {} : { userId: targetUserId }), ...(isSelf ? {} : { userId: targetUserId }),
}) })
}> }
>
<Text style={styles.value}> <Text style={styles.value}>
{isSelf ? followingCount : following} {isSelf ? followingCount : following}
</Text> </Text>
@@ -334,21 +339,23 @@ const Profile = () => {
style={{ style={{
height: 36, height: 36,
borderRadius: 100, borderRadius: 100,
overflow: 'hidden', overflow: "hidden",
backgroundColor: '#FFFFFF22', backgroundColor: "#FFFFFF22",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
width: '80%', width: "80%",
alignSelf: 'center', alignSelf: "center",
marginTop: 18, marginTop: 18,
}}> }}
>
<Text <Text
style={{ style={{
fontSize: 12, fontSize: 12,
color: Palette.white, color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
}}> }}
{isFollowing ? 'Ne plus suivre' : 'Suivre'} >
{isFollowing ? "Ne plus suivre" : "Suivre"}
</Text> </Text>
</View> </View>
</Pressable> </Pressable>
@@ -359,18 +366,20 @@ const Profile = () => {
style={{ style={{
...Style.containerRow, ...Style.containerRow,
gap: 6, gap: 6,
}}> }}
>
{/*{["Chansons", "Playbacks", "Clips"].map((item, index) => (*/} {/*{["Chansons", "Playbacks", "Clips"].map((item, index) => (*/}
{['Chansons', 'Playbacks'].map((item, index) => ( {["Chansons", "Playbacks"].map((item, index) => (
<Pressable <Pressable
key={index} key={index}
onPress={() => onPressMenu(item)} onPress={() => onPressMenu(item)}
style={{ flex: 1 }}> style={{ flex: 1 }}
>
<BorderGradient <BorderGradient
gradientProps={{ gradientProps={{
colors: colors:
selected === item selected === item
? ['#FFFFFF00', '#FFFFFF'] ? ["#FFFFFF00", "#FFFFFF"]
: [Palette.tran, Palette.tran], : [Palette.tran, Palette.tran],
locations: [0, 1], locations: [0, 1],
start: { x: 0, y: 0 }, start: { x: 0, y: 0 },
@@ -392,22 +401,22 @@ const Profile = () => {
</Pressable> </Pressable>
))} ))}
</View> </View>
{selected === 'Playbacks' && ( {selected === "Playbacks" && (
<MusicListSection <MusicListSection
data={displayedPlaybacks} data={displayedPlaybacks}
emptyText='Aucun playback pour le moment' emptyText="Aucun playback pour le moment"
/> />
)} )}
{selected === 'Chansons' && ( {selected === "Chansons" && (
<MusicListSection <MusicListSection
data={displayedProjects} data={displayedProjects}
emptyText='Aucune chanson pour le moment' emptyText="Aucune chanson pour le moment"
/> />
)} )}
{selected === 'Clips' && ( {selected === "Clips" && (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<EmptyText text={'Aucun clip pour le moment'} /> <EmptyText text={"Aucun clip pour le moment"} />
</View> </View>
)} )}
</View> </View>
@@ -437,20 +446,20 @@ const styles = StyleSheet.create({
borderWidth: 1, borderWidth: 1,
borderRadius: 10, borderRadius: 10,
height: 33, height: 33,
position: 'absolute', position: "absolute",
zIndex: 1, zIndex: 1,
width: '100%', width: "100%",
}, },
blurContainer: { blurContainer: {
height: 33, height: 33,
zIndex: -1, zIndex: -1,
borderRadius: 10, borderRadius: 10,
overflow: 'hidden', overflow: "hidden",
backgroundColor: Palette.glass, backgroundColor: Palette.glass,
}, },
blurView: { blurView: {
width: '100%', width: "100%",
height: '100%', height: "100%",
paddingHorizontal: 10, paddingHorizontal: 10,
...Style.containerCenter, ...Style.containerCenter,
}, },