116 lines
3.5 KiB
TypeScript
116 lines
3.5 KiB
TypeScript
/**
|
|
* Renders the Follow buttons of a user's profile and of a playlist's page.
|
|
* Both go through `FollowContext`, which follows at once and rolls back when
|
|
* the request fails; the button then says so beside it.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
import { useAuth } from "../../hooks/auth/useAuth.ts";
|
|
import { useFollows } from "../../hooks/social/useFollows.ts";
|
|
|
|
/** Passed to {@link FollowUserButton} by the `UserPublicProfile` page. */
|
|
export interface FollowUserButtonProps {
|
|
targetUserId: string;
|
|
/** Names the user in the button's accessible label. */
|
|
targetUsername: string;
|
|
}
|
|
|
|
/** Passed to {@link FollowPlaylistButton} by `PlaylistHeader`. */
|
|
export interface FollowPlaylistButtonProps {
|
|
targetPlaylistId: string;
|
|
/** Hides the button, since only a public playlist can be followed. */
|
|
isPublic: boolean;
|
|
}
|
|
|
|
/**
|
|
* Renders the button that follows or unfollows a user. It renders nothing for
|
|
* a visitor or on the user's own profile, and is disabled until the follows
|
|
* have loaded.
|
|
*/
|
|
export function FollowUserButton(
|
|
{ targetUserId, targetUsername }: FollowUserButtonProps,
|
|
): React.JSX.Element | null {
|
|
const { user } = useAuth();
|
|
const { followedUserIds, followUser, unfollowUser, isLoaded } = useFollows();
|
|
const [failed, setFailed] = useState(false);
|
|
|
|
if (!user || user.id === targetUserId) return null;
|
|
|
|
const isFollowing = followedUserIds.has(targetUserId);
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className={`follow-btn${isFollowing ? " follow-btn--following" : ""}`}
|
|
disabled={!isLoaded}
|
|
onClick={async () => {
|
|
setFailed(false);
|
|
const ok = await (isFollowing
|
|
? unfollowUser(targetUserId)
|
|
: followUser(targetUserId));
|
|
setFailed(!ok);
|
|
}}
|
|
aria-label={isFollowing
|
|
? t`Unfollow ${targetUsername}`
|
|
: t`Follow ${targetUsername}`}
|
|
>
|
|
{isFollowing ? <Trans>Following</Trans> : <Trans>Follow</Trans>}
|
|
</button>
|
|
{failed && <FollowError />}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders the button that follows or unfollows a playlist. It renders nothing
|
|
* for a visitor or a private playlist, and is disabled until the follows have
|
|
* loaded. Unlike {@link FollowUserButton}, it shows on the user's own
|
|
* playlists too.
|
|
*/
|
|
export function FollowPlaylistButton(
|
|
{ targetPlaylistId, isPublic }: FollowPlaylistButtonProps,
|
|
): React.JSX.Element | null {
|
|
const { user } = useAuth();
|
|
const { followedPlaylistIds, followPlaylist, unfollowPlaylist, isLoaded } =
|
|
useFollows();
|
|
const [failed, setFailed] = useState(false);
|
|
|
|
if (!user || !isPublic) return null;
|
|
|
|
const isFollowing = followedPlaylistIds.has(targetPlaylistId);
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className={`follow-btn${isFollowing ? " follow-btn--following" : ""}`}
|
|
disabled={!isLoaded}
|
|
onClick={async () => {
|
|
setFailed(false);
|
|
const ok = await (isFollowing
|
|
? unfollowPlaylist(targetPlaylistId)
|
|
: followPlaylist(targetPlaylistId));
|
|
setFailed(!ok);
|
|
}}
|
|
aria-label={isFollowing ? t`Unfollow playlist` : t`Follow playlist`}
|
|
>
|
|
{isFollowing ? <Trans>Following</Trans> : <Trans>Follow</Trans>}
|
|
</button>
|
|
{failed && <FollowError />}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function FollowError(): React.JSX.Element {
|
|
return (
|
|
<span className="follow-error" role="alert">
|
|
<Trans>Couldn't update. Try again.</Trans>
|
|
</span>
|
|
);
|
|
}
|