+3
apps/web/src/api/feed.ts
+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
}[];
+5
-2
apps/web/src/components/SignInModal/SignInModal.tsx
+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 }}>
+35
apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx
+35
apps/web/src/components/SongCover/InteractionBar/InteractionBar.tsx
···
1
+
import HeartOutline from "../../Icons/HeartOutline";
2
+
import HeartFilled from "../../Icons/Heart";
3
+
4
+
export interface InteractionBarProps {
5
+
likesCount: number;
6
+
liked: boolean;
7
+
onLike: () => void;
8
+
}
9
+
10
+
function InteractionBar({ likesCount, liked, onLike }: InteractionBarProps) {
11
+
return (
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]">
13
+
<div className="h-[40px] w-full flex items-center">
14
+
<span
15
+
className="cursor-pointer"
16
+
onClick={(e) => {
17
+
e.stopPropagation();
18
+
e.preventDefault();
19
+
onLike();
20
+
}}
21
+
>
22
+
{!liked && <HeartOutline color="#fff" />}
23
+
{liked && <HeartFilled color="#fff" />}
24
+
</span>
25
+
{likesCount > 0 && (
26
+
<span className="ml-[5px] mt-[-4px] text-sm text-white">
27
+
{likesCount}
28
+
</span>
29
+
)}
30
+
</div>
31
+
</div>
32
+
);
33
+
}
34
+
35
+
export default InteractionBar;
+3
apps/web/src/components/SongCover/InteractionBar/index.tsx
+3
apps/web/src/components/SongCover/InteractionBar/index.tsx
+51
-5
apps/web/src/components/SongCover/SongCover.tsx
+51
-5
apps/web/src/components/SongCover/SongCover.tsx
···
1
1
import { css } from "@emotion/react";
2
2
import styled from "@emotion/styled";
3
+
import InteractionBar from "./InteractionBar";
4
+
import useLike from "../../hooks/useLike";
5
+
import SignInModal from "../SignInModal";
6
+
import { useState } from "react";
3
7
4
8
const Cover = styled.img<{ size?: number }>`
5
9
border-radius: 8px;
6
10
height: 240px;
7
11
width: 240px;
8
-
margin-bottom: 10px;
12
+
margin-bottom: -5px;
9
13
${(props) =>
10
14
props.size &&
11
15
css`
···
50
54
51
55
export type SongCoverProps = {
52
56
cover: string;
57
+
uri?: string;
53
58
title?: string;
54
59
artist?: string;
55
60
size?: number;
61
+
liked?: boolean;
62
+
likesCount?: number;
63
+
withLikeButton?: boolean;
56
64
};
57
65
58
66
function SongCover(props: SongCoverProps) {
59
-
const { title, artist, cover, size } = props;
67
+
const [isSignInOpen, setIsSignInOpen] = useState(false);
68
+
const [liked, setLiked] = useState(props.liked);
69
+
const { like, unlike } = useLike();
70
+
const [likesCount, setLikesCount] = useState(props.likesCount);
71
+
const { title, artist, cover, size, uri, withLikeButton } = props;
72
+
const handleLike = async () => {
73
+
if (!uri) return;
74
+
if (!localStorage.getItem("token")) {
75
+
setIsSignInOpen(true);
76
+
return;
77
+
}
78
+
if (liked) {
79
+
setLiked(false);
80
+
if (likesCount !== undefined && likesCount > 0) {
81
+
setLikesCount(likesCount - 1);
82
+
}
83
+
await unlike(uri);
84
+
} else {
85
+
setLiked(true);
86
+
if (likesCount !== undefined) {
87
+
setLikesCount(likesCount + 1);
88
+
}
89
+
await like(uri);
90
+
}
91
+
};
60
92
return (
61
-
<CoverWrapper>
62
-
<Cover src={cover} size={size} />
63
-
<div className="mb-[13px]">
93
+
<CoverWrapper onClick={(e) => e.stopPropagation()}>
94
+
<div className={`relative h-[100%] w-[92%]`}>
95
+
{withLikeButton && (
96
+
<InteractionBar
97
+
liked={!!liked}
98
+
likesCount={likesCount || 0}
99
+
onLike={handleLike}
100
+
/>
101
+
)}
102
+
<Cover src={cover} size={size} />
103
+
</div>
104
+
<div className="mb-[13px] mt-[10px]">
64
105
<SongTitle className="!text-[var(--color-text-primary)]">
65
106
{title}
66
107
</SongTitle>
67
108
<Artist>{artist}</Artist>
68
109
</div>
110
+
<SignInModal
111
+
isOpen={isSignInOpen}
112
+
onClose={() => setIsSignInOpen(false)}
113
+
like
114
+
/>
69
115
</CoverWrapper>
70
116
);
71
117
}
+5
-1
apps/web/src/pages/home/feed/Feed.tsx
+5
-1
apps/web/src/pages/home/feed/Feed.tsx
···
76
76
}
77
77
console.log(">> WebSocket connection closed");
78
78
};
79
-
}, [queryClient]);
79
+
}, [queryClient, feedUri]);
80
80
81
81
return (
82
82
<Container>
···
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}
135
+
withLikeButton
132
136
/>
133
137
</Link>
134
138
<div className="flex">
Submissions
4 commits
expand
collapse
feat: add InteractionBar component and integrate with SongCover for like functionality
work in progress
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
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.
pull request successfully merged