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

Prompt sign-in for likes and stop click propagation

Show SignInModal (with a `like` flag) when a user attempts to like
without a token. Add optimistic local state for liked and likesCount to
update UI immediately. Stop event propagation on the like button and
cover to prevent parent click handlers.

authored by tsiry-sandratraina.com and committed by tangled.org 44d7ef3a 4bcc5391

Changed files
+30 -5
apps
web
src
components
SignInModal
SongCover
+5 -2
apps/web/src/components/SignInModal/SignInModal.tsx
··· 7 7 interface SignInModalProps { 8 8 isOpen: boolean; 9 9 onClose: () => void; 10 + like?: boolean; 10 11 } 11 12 12 13 function SignInModal(props: SignInModalProps) { 13 - const { isOpen, onClose } = props; 14 + const { isOpen, onClose, like } = props; 14 15 const [handle, setHandle] = useState(""); 15 16 16 17 const onLogin = async () => { ··· 55 56 <ModalBody style={{ padding: 10 }}> 56 57 <h1 style={{ color: "#ff2876", textAlign: "center" }}>Rocksky</h1> 57 58 <p className="text-[var(--color-text)] text-[18px] mt-[40px] mb-[20px]"> 58 - Sign in or create your account to join the conversation! 59 + {!like 60 + ? "Sign in or create your account to join the conversation!" 61 + : "Sign in or create your account to like songs!"} 59 62 </p> 60 63 <div style={{ marginBottom: 20 }}> 61 64 <div style={{ marginBottom: 15 }}>
+1
apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx
··· 14 14 <span 15 15 className="cursor-pointer" 16 16 onClick={(e) => { 17 + e.stopPropagation(); 17 18 e.preventDefault(); 18 19 onLike(); 19 20 }}
+24 -3
apps/web/src/components/SongCover/SongCover.tsx
··· 2 2 import styled from "@emotion/styled"; 3 3 import InteractionBar from "./InteractionBar"; 4 4 import useLike from "../../hooks/useLike"; 5 + import SignInModal from "../SignInModal"; 6 + import { useState } from "react"; 5 7 6 8 const Cover = styled.img<{ size?: number }>` 7 9 border-radius: 8px; ··· 62 64 }; 63 65 64 66 function SongCover(props: SongCoverProps) { 67 + const [isSignInOpen, setIsSignInOpen] = useState(false); 68 + const [liked, setLiked] = useState(props.liked); 65 69 const { like, unlike } = useLike(); 66 - const { title, artist, cover, size, liked, likesCount, uri, withLikeButton } = 67 - props; 70 + const [likesCount, setLikesCount] = useState(props.likesCount); 71 + const { title, artist, cover, size, uri, withLikeButton } = props; 68 72 const handleLike = async () => { 69 73 if (!uri) return; 74 + if (!localStorage.getItem("token")) { 75 + setIsSignInOpen(true); 76 + return; 77 + } 70 78 if (liked) { 79 + setLiked(false); 80 + if (likesCount !== undefined && likesCount > 0) { 81 + setLikesCount(likesCount - 1); 82 + } 71 83 await unlike(uri); 72 84 } else { 85 + setLiked(true); 86 + if (likesCount !== undefined) { 87 + setLikesCount(likesCount + 1); 88 + } 73 89 await like(uri); 74 90 } 75 91 }; 76 92 return ( 77 - <CoverWrapper> 93 + <CoverWrapper onClick={(e) => e.stopPropagation()}> 78 94 <div className={`relative h-[100%] w-[92%]`}> 79 95 {withLikeButton && ( 80 96 <InteractionBar ··· 91 107 </SongTitle> 92 108 <Artist>{artist}</Artist> 93 109 </div> 110 + <SignInModal 111 + isOpen={isSignInOpen} 112 + onClose={() => setIsSignInOpen(false)} 113 + like 114 + /> 94 115 </CoverWrapper> 95 116 ); 96 117 }