Fixed the like button and now desactivated the click on card to go to song and changed default display for score from '?' to '-'

This commit is contained in:
Clément Le Bihan
2023-12-27 21:44:16 +01:00
parent c5124fa6ad
commit af0531bb0c
7 changed files with 358 additions and 196 deletions
+135
View File
@@ -0,0 +1,135 @@
// credit to https://gist.github.com/ianmartorell/32bb7df95e5eff0a5ee2b2f55095e6a6
// this file was repurosed from there
// via this issue https://gist.github.com/necolas/1c494e44e23eb7f8c5864a2fac66299a
// because RNW's pressable doesn't bubble events to parent pressables: https://github.com/necolas/react-native-web/issues/1875
/* eslint-disable no-inner-declarations */
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
let isEnabled = false;
if (canUseDOM) {
/**
* Web browsers emulate mouse events (and hover states) after touch events.
* This code infers when the currently-in-use modality supports hover
* (including for multi-modality devices) and considers "hover" to be enabled
* if a mouse movement occurs more than 1 second after the last touch event.
* This threshold is long enough to account for longer delays between the
* browser firing touch and mouse events on low-powered devices.
*/
const HOVER_THRESHOLD_MS = 1000;
let lastTouchTimestamp = 0;
function enableHover() {
if (isEnabled || Date.now() - lastTouchTimestamp < HOVER_THRESHOLD_MS) {
return;
}
isEnabled = true;
}
function disableHover() {
lastTouchTimestamp = Date.now();
if (isEnabled) {
isEnabled = false;
}
}
document.addEventListener('touchstart', disableHover, true);
document.addEventListener('touchmove', disableHover, true);
document.addEventListener('mousemove', enableHover, true);
}
function isHoverEnabled(): boolean {
return isEnabled;
}
import React, { useCallback, ReactChild, useRef } from 'react';
import { useSharedValue, useAnimatedReaction } from 'react-native-reanimated';
import { Platform } from 'react-native';
export interface HoverableProps {
onHoverIn?: () => void;
onHoverOut?: () => void;
onPressIn?: () => void;
onPressOut?: () => void;
children: NonNullable<ReactChild>;
}
export default function Hoverable({
onHoverIn,
onHoverOut,
children,
onPressIn,
onPressOut,
}: HoverableProps) {
const showHover = useSharedValue(true);
const isHovered = useSharedValue(false);
const hoverIn = useRef<undefined | (() => void)>(() => onHoverIn?.());
const hoverOut = useRef<undefined | (() => void)>(() => onHoverOut?.());
const pressIn = useRef<undefined | (() => void)>(() => onPressIn?.());
const pressOut = useRef<undefined | (() => void)>(() => onPressOut?.());
hoverIn.current = onHoverIn;
hoverOut.current = onHoverOut;
pressIn.current = onPressIn;
pressOut.current = onPressOut;
useAnimatedReaction(
() => {
return Platform.OS === 'web' && showHover.value && isHovered.value;
},
(hovered, previouslyHovered) => {
if (hovered !== previouslyHovered) {
if (hovered && hoverIn.current) {
// no need for runOnJS, it's always web
hoverIn.current();
} else if (hoverOut.current) {
hoverOut.current();
}
}
},
[]
);
const handleMouseEnter = useCallback(() => {
if (isHoverEnabled() && !isHovered.value) {
isHovered.value = true;
}
}, [isHovered]);
const handleMouseLeave = useCallback(() => {
if (isHovered.value) {
isHovered.value = false;
}
}, [isHovered]);
const handleGrant = useCallback(() => {
showHover.value = false;
pressIn.current?.();
}, [showHover]);
const handleRelease = useCallback(() => {
showHover.value = true;
pressOut.current?.();
}, [showHover]);
let webProps = {};
if (Platform.OS === 'web') {
webProps = {
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave,
// prevent hover showing while responder
onResponderGrant: handleGrant,
onResponderRelease: handleRelease,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return React.cloneElement(React.Children.only(children) as any, {
...webProps,
// if child is Touchable
onPressIn: handleGrant,
onPressOut: handleRelease,
});
}
+2 -2
View File
@@ -195,12 +195,12 @@ const IconButton: React.FC<IconButtonProps> = ({
Animated.timing(scaleValue, {
toValue: scaleFactor,
duration: animationDuration,
useNativeDriver: true,
useNativeDriver: false,
}),
Animated.timing(scaleValue, {
toValue: 1,
duration: animationDuration,
useNativeDriver: true,
useNativeDriver: false,
}),
]),
];
+1 -1
View File
@@ -124,7 +124,7 @@ const InteractiveCC: React.FC<InteractiveCCProps> = ({
Animated.timing(animatedValues[key]!, {
toValue: stateValue,
duration: duration,
useNativeDriver: true,
useNativeDriver: false,
}).start();
});
};
+181 -192
View File
@@ -1,12 +1,21 @@
import Song from '../../models/Song';
import React from 'react';
import { Image, View } from 'react-native';
import { Pressable, Text, IconButton, PresenceTransition, Icon, useBreakpointValue } from 'native-base';
import { Image, Platform, View } from 'react-native';
import {
Pressable,
Text,
IconButton,
PresenceTransition,
Icon,
useBreakpointValue,
} from 'native-base';
import { LikeButton } from './SongCardInfoLikeBtn';
import { Ionicons } from '@expo/vector-icons';
import { useQuery } from '../../Queries';
import API from '../../API';
import { MaterialIcons } from '@expo/vector-icons';
import { useLikeSongMutation } from '../../utils/likeSongMutation';
import Hoverable from '../Hoverable';
type SongCardInfoProps = {
song: Song;
@@ -21,7 +30,7 @@ const SongCardInfo = (props: SongCardInfoProps) => {
const [isHovered, setIsHovered] = React.useState(false);
const [isSlided, setIsSlided] = React.useState(false);
const user = useQuery(API.getUserInfo);
const isLiked = (props.song.likedByUsers ?? []).filter(({ userId }) => userId === user.data?.id).length > 0;
const [isLiked, setIsLiked] = React.useState(false);
const { mutate } = useLikeSongMutation();
const CardDims = {
@@ -32,16 +41,27 @@ const SongCardInfo = (props: SongCardInfoProps) => {
const Scores = [
{
icon: 'time',
score: props.song.lastScore ?? '?',
score: props.song.lastScore ?? '-',
},
{
icon: 'trophy',
score: props.song.bestScore ?? '?',
score: props.song.bestScore ?? '-',
},
];
React.useEffect(() => {
if (!user.data) {
return;
}
setIsLiked(
props.song.likedByUsers?.some(({ userId }) => userId === user.data?.id) ?? false
);
}, [user.data, props.song.likedByUsers]);
return (
<View
<Pressable
delayHoverIn={7}
onPress={Platform.OS === 'android' ? props.onPress : undefined}
style={{
width: CardDims.width,
height: CardDims.height,
@@ -51,232 +71,201 @@ const SongCardInfo = (props: SongCardInfoProps) => {
backgroundColor: 'rgba(16, 16, 20, 0.70)',
borderRadius: 12,
overflow: 'hidden',
position: 'relative',
}}
onHoverIn={() => {
setIsHovered(true);
}}
onHoverOut={() => {
setIsHovered(false);
setIsSlided(false);
}}
>
<Pressable
delayHoverIn={7}
isHovered={isPlayHovered ? true : undefined}
onPress={props.onPress}
<View
style={{
width: '100%',
}}
onHoverIn={() => {
setIsHovered(true);
}}
onHoverOut={() => {
setIsHovered(false);
setIsSlided(false);
width: CardDims.width,
height: CardDims.height,
backgroundColor: 'rgba(16, 16, 20, 0.7)',
borderRadius: 12,
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
<>
<View
<View
style={{
width: '100%',
marginBottom: 8,
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{Scores.map((score, idx) => (
<View
key={score.icon + idx}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 5,
paddingHorizontal: 10,
}}
>
<Icon as={Ionicons} name={score.icon} size={17} color="white" />
<Text
style={{
color: 'white',
fontSize: 12,
fontWeight: 'bold',
}}
>
{score.score}
</Text>
</View>
))}
</View>
</View>
<PresenceTransition
style={{
width: '100%',
height: '100%',
position: 'absolute',
}}
visible={isHovered}
animate={{
translateY: -55,
}}
onTransitionComplete={() => {
if (isHovered) {
setIsSlided(true);
}
}}
>
<View
style={{
width: CardDims.width,
height: CardDims.height,
position: 'relative',
}}
>
<Image
source={{ uri: props.song.cover }}
style={{
width: CardDims.width,
height: CardDims.height,
backgroundColor: 'rgba(16, 16, 20, 0.7)',
borderRadius: 12,
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
<View
style={{
width: '100%',
marginBottom: 8,
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
{Scores.map((score, idx) => (
<View
key={score.icon + idx}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 5,
paddingHorizontal: 10,
}}
>
<Icon as={Ionicons} name={score.icon} size={17} color="white" />
<Text
style={{
color: 'white',
fontSize: 12,
fontWeight: 'bold',
}}
>
{score.score}
</Text>
</View>
))}
</View>
</View>
<PresenceTransition
/>
<View
style={{
position: 'absolute',
width: '100%',
height: '100%',
position: 'absolute',
}}
visible={isHovered}
initial={{
translateY: 0,
}}
animate={{
translateY: -55,
}}
onTransitionComplete={() => {
if (isHovered) {
setIsSlided(true);
}
backgroundColor: 'rgba(0, 0, 0, 0.75)',
justifyContent: 'flex-end',
alignItems: 'flex-start',
paddingHorizontal: 10,
paddingVertical: 7,
borderRadius: 12,
}}
>
<Image
source={{ uri: props.song.cover }}
style={{
position: 'relative',
width: CardDims.width,
height: CardDims.height,
borderRadius: 12,
}}
/>
<View
style={{
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
justifyContent: 'flex-end',
alignItems: 'flex-start',
paddingHorizontal: 10,
paddingVertical: 7,
borderRadius: 12,
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
}}
>
<View
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
flexShrink: 1,
}}
>
<View
<Text
numberOfLines={2}
style={{
flexShrink: 1,
color: 'white',
fontSize: 14,
fontWeight: 'bold',
marginBottom: 4,
}}
>
<Text
numberOfLines={2}
style={{
color: 'white',
fontSize: 14,
fontWeight: 'bold',
marginBottom: 4,
}}
>
{props.song.name}
</Text>
<Text
numberOfLines={1}
style={{
color: 'white',
fontSize: 12,
fontWeight: 'normal',
}}
>
{props.song.artist?.name}
</Text>
</View>
<IconButton
variant={'ghost'}
borderRadius={'full'}
size={17}
color="#6075F9"
onPress={async () => {
mutate({ songId: props.song.id, like: !isLiked });
{props.song.name}
</Text>
<Text
numberOfLines={1}
style={{
color: 'white',
fontSize: 12,
fontWeight: 'normal',
}}
_icon={{
as: MaterialIcons,
name: isLiked ? 'favorite' : 'favorite-outline',
}}
/>
>
{props.song.artist?.name}
</Text>
</View>
<LikeButton
color="#6075F9"
onPress={() => {
console.log('like');
mutate({ songId: props.song.id, like: !isLiked });
}}
isLiked={isLiked}
/>
</View>
</PresenceTransition>
<PresenceTransition
style={{
width: '100%',
height: '100%',
position: 'absolute',
</View>
</View>
</PresenceTransition>
{Platform.OS === 'web' && (
<PresenceTransition
style={{
position: 'absolute',
bottom: 35,
left: CardDims.width / 2 - 20,
}}
visible={isSlided}
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
>
<Hoverable
onHoverIn={() => {
setIsPlayHovered(true);
}}
visible={isSlided}
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
onHoverOut={() => {
setIsPlayHovered(false);
}}
>
<View
onClick={(e) => {
e.stopPropagation();
props.onPress();
}}
style={{
position: 'absolute',
width: '100%',
height: '100%',
width: 40,
height: 40,
borderRadius: 100,
display: 'flex',
justifyContent: 'flex-end',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: isPlayHovered
? 'rgba(96, 117, 249, 0.9)'
: 'rgba(96, 117, 249, 0.7)',
}}
>
<Pressable
onHoverIn={() => {
setIsPlayHovered(true);
}}
onHoverOut={() => {
setIsPlayHovered(false);
}}
borderRadius={100}
marginBottom={35}
onPress={props.onPlay}
>
{({ isPressed, isHovered }) => (
<View
style={{
width: 40,
height: 40,
borderRadius: 100,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: (() => {
if (isPressed) {
return 'rgba(96, 117, 249, 1)';
} else if (isHovered) {
return 'rgba(96, 117, 249, 0.9)';
} else {
return 'rgba(96, 117, 249, 0.7)';
}
})(),
}}
>
<Ionicons
name="play-outline"
color={'white'}
size={20}
rounded="sm"
/>
</View>
)}
</Pressable>
<Ionicons name="play-outline" color={'white'} size={20} rounded="sm" />
</View>
</PresenceTransition>
</>
</Pressable>
</View>
</Hoverable>
</PresenceTransition>
)}
</Pressable>
);
};
@@ -0,0 +1,37 @@
import { Platform, View } from 'react-native';
import { IconButton } from 'native-base';
import { MaterialIcons } from '@expo/vector-icons';
type LikeButtonProps = {
isLiked: boolean;
onPress?: () => void;
color?: string;
};
export const LikeButton = ({ isLiked, color, onPress }: LikeButtonProps) => {
if (Platform.OS === 'web') {
// painful error of no onHover event control
return (
<View onClick={onPress}>
<MaterialIcons
color={color}
name={isLiked ? 'favorite' : 'favorite-outline'}
size={17}
/>
</View>
);
}
return (
<IconButton
variant={'ghost'}
borderRadius={'full'}
size={17}
color={color}
onPress={onPress}
_icon={{
as: MaterialIcons,
name: isLiked ? 'favorite' : 'favorite-outline',
}}
/>
);
};