sendou.ink/app/features/art/components/ArtGrid.tsx
2023-07-09 21:02:32 +03:00

237 lines
6.0 KiB
TypeScript

import { Link } from "@remix-run/react";
import Masonry, { ResponsiveMasonry } from "react-responsive-masonry";
import { Avatar } from "~/components/Avatar";
import { useIsMounted } from "~/hooks/useIsMounted";
import { discordFullName } from "~/utils/strings";
import {
conditionalUserSubmittedImage,
newArtPage,
userArtPage,
} from "~/utils/urls";
import type { ListedArt } from "../art-types";
import { Dialog } from "~/components/Dialog";
import * as React from "react";
import { useSimplePagination } from "~/hooks/useSimplePagination";
import { ART_PER_PAGE } from "../art-constants";
import { Button, LinkButton } from "~/components/Button";
import { ArrowRightIcon } from "~/components/icons/ArrowRight";
import { ArrowLeftIcon } from "~/components/icons/ArrowLeft";
import { nullFilledArray } from "~/utils/arrays";
import clsx from "clsx";
import { EditIcon } from "~/components/icons/Edit";
import { useTranslation } from "~/hooks/useTranslation";
import { previewUrl } from "../art-utils";
import { TrashIcon } from "~/components/icons/Trash";
import { FormWithConfirm } from "~/components/FormWithConfirm";
export function ArtGrid({
arts,
enablePreview = false,
canEdit = false,
}: {
arts: ListedArt[];
enablePreview?: boolean;
canEdit?: boolean;
}) {
const {
itemsToDisplay,
everythingVisible,
currentPage,
pagesCount,
nextPage,
previousPage,
} = useSimplePagination({
items: arts,
pageSize: ART_PER_PAGE,
});
const [bigArt, setBigArt] = React.useState<ListedArt | null>(null);
const isMounted = useIsMounted();
if (!isMounted) return null;
return (
<>
{bigArt ? (
<BigImageDialog close={() => setBigArt(null)} art={bigArt} />
) : null}
<ResponsiveMasonry columnsCountBreakPoints={{ 350: 1, 750: 2, 900: 3 }}>
<Masonry gutter="1rem">
{itemsToDisplay.map((art) => (
<ImagePreview
key={art.id}
art={art}
canEdit={canEdit}
enablePreview={enablePreview}
onClick={enablePreview ? () => setBigArt(art) : undefined}
/>
))}
</Masonry>
</ResponsiveMasonry>
{!everythingVisible ? (
<SimplePagination
currentPage={currentPage}
pagesCount={pagesCount}
nextPage={nextPage}
previousPage={previousPage}
/>
) : null}
</>
);
}
function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) {
const [imageLoaded, setImageLoaded] = React.useState(false);
return (
<Dialog
isOpen
close={close}
className="art__dialog__image-container"
closeOnAnyClick
>
<img
alt=""
src={conditionalUserSubmittedImage(art.url)}
loading="lazy"
className="art__dialog__img"
onLoad={() => setImageLoaded(true)}
/>
{art.description ? (
<div
className={clsx("art__dialog__description", {
invisible: !imageLoaded,
})}
>
{art.description}
</div>
) : null}
</Dialog>
);
}
function ImagePreview({
art,
onClick,
enablePreview = false,
canEdit = false,
}: {
art: ListedArt;
onClick?: () => void;
enablePreview?: boolean;
canEdit?: boolean;
}) {
const [imageLoaded, setImageLoaded] = React.useState(false);
const { t } = useTranslation(["common", "art"]);
const img = (
<img
alt=""
src={conditionalUserSubmittedImage(previewUrl(art.url))}
loading="lazy"
onClick={onClick}
onLoad={() => setImageLoaded(true)}
className={enablePreview ? "art__thumbnail" : undefined}
/>
);
if (!art.author && canEdit) {
return (
<div>
{img}
<div
className={clsx("stack horizontal justify-between mt-2", {
invisible: !imageLoaded,
})}
>
<LinkButton
to={newArtPage(art.id)}
size="tiny"
variant="outlined"
icon={<EditIcon />}
>
{t("common:actions.edit")}
</LinkButton>
<FormWithConfirm
dialogHeading="Are you sure you want to delete the art?"
fields={[["id", art.id]]}
>
<Button icon={<TrashIcon />} variant="destructive" size="tiny" />
</FormWithConfirm>
</div>
</div>
);
}
if (!art.author) return img;
// whole thing is not a link so we can preview the image
if (enablePreview) {
return (
<div>
{img}
<Link
to={userArtPage(art.author, "MADE-BY")}
className={clsx("stack sm horizontal text-xs items-center mt-1", {
invisible: !imageLoaded,
})}
>
<Avatar user={art.author} size="xxs" />
{t("art:madeBy")} {discordFullName(art.author)}
</Link>
</div>
);
}
return (
<Link to={userArtPage(art.author, "MADE-BY")}>
{img}
<div
className={clsx("stack sm horizontal text-xs items-center mt-1", {
invisible: !imageLoaded,
})}
>
<Avatar user={art.author} size="xxs" />
{discordFullName(art.author)}
</div>
</Link>
);
}
function SimplePagination({
currentPage,
pagesCount,
nextPage,
previousPage,
}: {
currentPage: number;
pagesCount: number;
nextPage: () => void;
previousPage: () => void;
}) {
return (
<div className="stack sm horizontal items-center justify-center flex-wrap">
<Button
icon={<ArrowLeftIcon />}
variant="outlined"
disabled={currentPage === 1}
onClick={previousPage}
aria-label="Previous page"
/>
{nullFilledArray(pagesCount).map((_, i) => (
<div
key={i}
className={clsx("pagination__dot", {
pagination__dot__active: i === currentPage - 1,
})}
/>
))}
<Button
icon={<ArrowRightIcon />}
variant="outlined"
disabled={currentPage === pagesCount}
onClick={nextPage}
aria-label="Next page"
/>
</div>
);
}