Files
gerbeur/src/pages/PlaylistDetail.tsx

147 lines
4.3 KiB
TypeScript

/**
* Renders a playlist's page, where its owner and moderators also edit it and
* reorder its dumps.
*
* @module
*/
import { useState } from "react";
import { useNavigate, useParams } from "react-router";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { can } from "../utils/auth/permissions.ts";
import { useAuth } from "../hooks/auth/useAuth.ts";
import { useDocumentTitle } from "../hooks/shared/useDocumentTitle.ts";
import { useHeightTransition } from "../hooks/shared/useHeightTransition.ts";
import { usePlaylist } from "../hooks/playlist/usePlaylist.ts";
import { PageShell } from "../components/layout/PageShell.tsx";
import { PageError } from "../components/layout/PageError.tsx";
import { ErrorCard } from "../components/shared/ErrorCard.tsx";
import { ConfirmModal } from "../components/shared/ConfirmModal.tsx";
import { PlaylistHeader } from "../components/playlist/PlaylistHeader.tsx";
import { PlaylistEditForm } from "../components/playlist/PlaylistEditForm.tsx";
import { PlaylistDumpList } from "../components/playlist/PlaylistDumpList.tsx";
/**
* Renders `/@:username/playlists/:slug`, for anyone who may see the playlist:
* a private one only to its owner. `usePlaylist` loads it, by a former name or
* slug too, and keeps it live. Its owner, or a user with `playlist:moderate`, can edit or
* delete it, drag its dumps into a new order, and remove one, with a short
* window to undo.
*/
export function PlaylistDetail(): React.JSX.Element {
const { handle, slug } = useParams();
const navigate = useNavigate();
const { user } = useAuth();
const {
state,
fading,
actionError,
applyDetails,
remove,
undoRemove,
move,
saveOrder,
deletePlaylist,
} = usePlaylist(handle?.slice(1), slug);
const [editOpen, setEditOpen] = useState(false);
const headerRef = useHeightTransition<HTMLDivElement>(editOpen);
const [confirmDelete, setConfirmDelete] = useState(false);
useDocumentTitle(
state.status === "loaded"
? state.data.title
: state.status === "error"
? null
: undefined,
);
if (state.status === "loading") {
return (
<PageShell>
<p className="page-loading">
<Trans>Loading playlist…</Trans>
</p>
</PageShell>
);
}
if (state.status === "error") {
return (
<PageError
message={state.error}
actions={
<button
className="btn-border"
type="button"
onClick={() => navigate("/")}
>
<Trans>← Back</Trans>
</button>
}
/>
);
}
const playlist = state.data;
const canEdit = (!!user && user.id === playlist.userId) ||
can(user, "playlist:moderate");
return (
<PageShell>
<div className="playlist-detail-header" ref={headerRef}>
{editOpen
? (
<PlaylistEditForm
playlist={playlist}
onCancel={() => setEditOpen(false)}
onRequestDelete={() => setConfirmDelete(true)}
onSaved={(updated) => {
setEditOpen(false);
// Shown at once rather than left to the socket's echo,
// which may never come (disconnected).
applyDetails(updated);
}}
/>
)
: (
<div className="playlist-detail-header-top">
<PlaylistHeader
playlist={playlist}
onEdit={canEdit ? () => setEditOpen(true) : undefined}
/>
</div>
)}
</div>
{actionError && (
<ErrorCard title={t`Something went wrong`} message={actionError} />
)}
<PlaylistDumpList
dumps={playlist.dumps}
fading={fading}
{...(canEdit && {
onRemove: remove,
onUndoRemove: undoRemove,
onMove: move,
onMoved: saveOrder,
})}
/>
{confirmDelete && (
<ConfirmModal
message={t`Delete this playlist? This cannot be undone.`}
confirmLabel={t`Delete playlist`}
onConfirm={() => {
setConfirmDelete(false);
void deletePlaylist();
}}
onCancel={() => setConfirmDelete(false)}
/>
)}
</PageShell>
);
}