147 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|