140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
/**
|
|
* Renders the card that shows a playlist in profiles, search results and the
|
|
* dump page's "In collections" section.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { Link, useNavigate } from "react-router";
|
|
import { t } from "@lingui/core/macro";
|
|
import { Plural, Trans } from "@lingui/react/macro";
|
|
import type { Playlist } from "../../../shared/playlist.ts";
|
|
import { relativeTime } from "../../utils/shared/relativeTime.ts";
|
|
import { playlistImageUrl, playlistUrl } from "../../utils/shared/urls.ts";
|
|
import { useAuth } from "../../hooks/auth/useAuth.ts";
|
|
import {
|
|
isPlaylistVisited,
|
|
isRecent,
|
|
markPlaylistVisited,
|
|
} from "../../utils/shared/visited.ts";
|
|
import { Tooltip } from "../shared/Tooltip.tsx";
|
|
|
|
/** Passed to {@link PlaylistCard} by each list of playlists. */
|
|
export interface PlaylistCardProps {
|
|
playlist: Playlist;
|
|
/**
|
|
* Shows a delete button, which calls it. The owner's playlists pages pass
|
|
* one that asks for confirmation first.
|
|
*/
|
|
onDelete?: () => void;
|
|
/** Hides the owner link and the unread dot. */
|
|
isOwner?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Renders a playlist as a card that opens the playlist when clicked. Another
|
|
* user's recent playlist shows an unread dot until it is opened on this
|
|
* device. The card is an `<li>`, for a `<ul>`.
|
|
*/
|
|
export function PlaylistCard(
|
|
{ playlist, onDelete, isOwner }: PlaylistCardProps,
|
|
): React.JSX.Element {
|
|
const navigate = useNavigate();
|
|
const { mediaToken } = useAuth();
|
|
const unread = !isOwner && isRecent(playlist.createdAt) &&
|
|
!isPlaylistVisited(playlist.id);
|
|
|
|
function handleNavigate() {
|
|
markPlaylistVisited(playlist.id);
|
|
navigate(playlistUrl(playlist));
|
|
}
|
|
|
|
return (
|
|
<li className="playlist-card">
|
|
<div
|
|
className="playlist-card-inner"
|
|
onClick={handleNavigate}
|
|
>
|
|
<div className="playlist-card-preview">
|
|
{playlist.imageMime
|
|
? (
|
|
<img
|
|
src={playlistImageUrl(playlist, mediaToken)}
|
|
alt=""
|
|
className="playlist-card-img"
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
)
|
|
: <span className="playlist-card-icon">📋</span>}
|
|
</div>
|
|
<div className="playlist-card-body">
|
|
<Link
|
|
to={playlistUrl(playlist)}
|
|
className="playlist-card-title"
|
|
onClick={(e) => {
|
|
// Or the card's handler navigates a second time.
|
|
e.stopPropagation();
|
|
markPlaylistVisited(playlist.id);
|
|
}}
|
|
>
|
|
{unread && <span className="unread-dot" aria-hidden="true" />}
|
|
{playlist.title}
|
|
</Link>
|
|
{playlist.description && (
|
|
<p className="playlist-card-description">{playlist.description}</p>
|
|
)}
|
|
<div className="playlist-card-meta">
|
|
<span
|
|
className={`playlist-badge${
|
|
playlist.isPublic ? "" : " playlist-badge--private"
|
|
}`}
|
|
>
|
|
{playlist.isPublic
|
|
? <Trans>public</Trans>
|
|
: <Trans>private</Trans>}
|
|
</span>
|
|
{playlist.ownerUsername && !isOwner && (
|
|
<Link
|
|
to={`/users/${playlist.ownerUsername}`}
|
|
className="playlist-card-owner"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
@{playlist.ownerUsername}
|
|
</Link>
|
|
)}
|
|
{playlist.dumpCount !== undefined && (
|
|
<span className="playlist-card-count">
|
|
<Plural
|
|
value={playlist.dumpCount}
|
|
one="# dump"
|
|
other="# dumps"
|
|
/>
|
|
</span>
|
|
)}
|
|
<Tooltip text={playlist.createdAt.toLocaleString()}>
|
|
<time dateTime={playlist.createdAt.toISOString()}>
|
|
{relativeTime(playlist.createdAt)}
|
|
</time>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{onDelete && (
|
|
<button
|
|
type="button"
|
|
className="playlist-card-delete-btn"
|
|
onClick={(e) => {
|
|
// Deleting must not open the playlist.
|
|
e.stopPropagation();
|
|
onDelete();
|
|
}}
|
|
aria-label={t`Delete playlist`}
|
|
>
|
|
✕
|
|
</button>
|
|
)}
|
|
</li>
|
|
);
|
|
}
|