A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz

Add likes support to feed and SongCover

Update feed API types to include trackUri, likesCount and liked Show
like button and counts in InteractionBar and expose onLike handler Use
useLike in SongCover to call like/unlike and pass uri/liked/likesCount
Forward like state from Feed to SongCover

+49 -6
+3
apps/web/src/api/feed.ts
··· 79 79 uri: string; 80 80 albumUri: string; 81 81 artistUri: string; 82 + trackUri: string; 82 83 xataVersion: number; 83 84 cover: string; 84 85 date: string; ··· 86 87 userDisplayName: string; 87 88 userAvatar: string; 88 89 tags: string[]; 90 + likesCount: number; 91 + liked: boolean; 89 92 id: string; 90 93 }; 91 94 }[];
+21 -4
apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx
··· 1 1 import HeartOutline from "../../Icons/HeartOutline"; 2 2 import HeartFilled from "../../Icons/Heart"; 3 3 4 - function InteractionBar() { 4 + export interface InteractionBarProps { 5 + likesCount: number; 6 + liked: boolean; 7 + onLike: () => void; 8 + } 9 + 10 + function InteractionBar({ likesCount, liked, onLike }: InteractionBarProps) { 5 11 return ( 6 12 <div className="absolute bottom-[-1px] left-0 h-[100px] w-full bg-[linear-gradient(rgba(22,24,35,0)_2.92%,rgba(22,24,35,0.5)_98.99%)] flex justify-start items-end p-[10px] rounded-b-[8px]"> 7 13 <div className="h-[40px] w-full flex items-center"> 8 - <span className="cursor-pointer" onClick={(e) => e.preventDefault()}> 9 - {true && <HeartOutline color="#fff" />} 10 - {false && <HeartFilled color="#fff" />} 14 + <span 15 + className="cursor-pointer" 16 + onClick={(e) => { 17 + e.preventDefault(); 18 + onLike(); 19 + }} 20 + > 21 + {!liked && <HeartOutline color="#fff" />} 22 + {liked && <HeartFilled color="#fff" />} 11 23 </span> 24 + {likesCount > 0 && ( 25 + <span className="ml-[5px] mt-[-4px] text-sm text-white"> 26 + {likesCount} 27 + </span> 28 + )} 12 29 </div> 13 30 </div> 14 31 );
+22 -2
apps/web/src/components/SongCover/SongCover.tsx
··· 1 1 import { css } from "@emotion/react"; 2 2 import styled from "@emotion/styled"; 3 3 import InteractionBar from "./InteractionBar"; 4 + import useLike from "../../hooks/useLike"; 4 5 5 6 const Cover = styled.img<{ size?: number }>` 6 7 border-radius: 8px; ··· 51 52 52 53 export type SongCoverProps = { 53 54 cover: string; 55 + uri?: string; 54 56 title?: string; 55 57 artist?: string; 56 58 size?: number; 59 + liked?: boolean; 60 + likesCount?: number; 57 61 withLikeButton?: boolean; 58 62 }; 59 63 60 64 function SongCover(props: SongCoverProps) { 61 - const { title, artist, cover, size, withLikeButton } = props; 65 + const { like, unlike } = useLike(); 66 + const { title, artist, cover, size, liked, likesCount, uri, withLikeButton } = 67 + props; 68 + const handleLike = async () => { 69 + if (!uri) return; 70 + if (liked) { 71 + await unlike(uri); 72 + } else { 73 + await like(uri); 74 + } 75 + }; 62 76 return ( 63 77 <CoverWrapper> 64 78 <div className={`relative h-[100%] w-[92%]`}> 65 - {withLikeButton && <InteractionBar />} 79 + {withLikeButton && ( 80 + <InteractionBar 81 + liked={!!liked} 82 + likesCount={likesCount || 0} 83 + onLike={handleLike} 84 + /> 85 + )} 66 86 <Cover src={cover} size={size} /> 67 87 </div> 68 88 <div className="mb-[13px] mt-[10px]">
+3
apps/web/src/pages/home/feed/Feed.tsx
··· 126 126 className="no-underline text-[var(--color-text-primary)]" 127 127 > 128 128 <SongCover 129 + uri={song.trackUri} 129 130 cover={song.cover} 130 131 artist={song.artist} 131 132 title={song.title} 133 + liked={song.liked} 134 + likesCount={song.likesCount} 132 135 withLikeButton 133 136 /> 134 137 </Link>