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:
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -124,7 +124,7 @@ const InteractiveCC: React.FC<InteractiveCCProps> = ({
|
||||
Animated.timing(animatedValues[key]!, {
|
||||
toValue: stateValue,
|
||||
duration: duration,
|
||||
useNativeDriver: true,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user