"use client";
import { useEntity, useReplicache } from "src/replicache";
import { BlockProps, BlockLayout } from "./Block";
import { useUIState } from "src/useUIState";
import Image from "next/image";
import { v7 } from "uuid";
import { useEntitySetContext } from "components/EntitySetProvider";
import { generateKeyBetween } from "fractional-indexing";
import { addImage, localImages } from "src/utils/addImage";
import { setCoverImageFromEntity } from "src/utils/setCoverImageFromEntity";
import { elementId } from "src/utils/elementId";
import { useEffect, useState } from "react";
import { BlockImageSmall } from "components/Icons/BlockImageSmall";
import { EditTiny } from "components/Icons/EditTiny";
import { set } from "colorjs.io/fn";
import { ImageAltButton } from "./ImageAltButton";
import {
ImageGalleryLightbox,
EditorLightboxSlide,
} from "./ImageGalleryBlock/ImageGalleryLightbox";
import {
useLeafletPublicationData,
useLeafletPublicationPage,
} from "components/PageSWRDataProvider";
import {
ImageCoverImage,
ImageCoverImageRemove,
} from "components/Icons/ImageCoverImage";
import {
ButtonPrimary,
ButtonSecondary,
ButtonTertiary,
} from "components/Buttons";
import { CheckTiny } from "components/Icons/CheckTiny";
export function ImageBlock(props: BlockProps & { preview?: boolean }) {
let { rep, undoManager } = useReplicache();
let image = useEntity(props.value, "block/image");
let entity_set = useEntitySetContext();
let isSelected = useUIState((s) =>
s.selectedBlocks.find((b) => b.value === props.value),
);
let isFullBleed = useEntity(props.value, "image/full-bleed")?.data.value;
let isFirst = props.previousBlock === null;
let isLast = props.nextBlock === null;
let altText = useEntity(props.value, "image/alt")?.data.value;
let [lightboxOpen, setLightboxOpen] = useState(false);
// Writers select the block first; a second click on the image opens the
// lightbox. Readers (no write permission) open it on the first click.
let canOpenLightbox =
!props.preview && (!entity_set.permissions.write || !!isSelected);
let nextIsFullBleed = useEntity(
props.nextBlock && props.nextBlock.value,
"image/full-bleed",
)?.data.value;
let prevIsFullBleed = useEntity(
props.previousBlock && props.previousBlock.value,
"image/full-bleed",
)?.data.value;
useEffect(() => {
if (props.preview) return;
let input = document.getElementById(elementId.block(props.entityID).input);
if (isSelected) {
input?.focus();
} else {
input?.blur();
}
}, [isSelected, props.preview, props.entityID]);
const handleImageUpload = async (file: File) => {
if (!rep) return;
await undoManager.withUndoGroup(async () => {
let entity = props.entityID;
if (!entity) {
entity = v7();
await rep?.mutate.addBlock({
parent: props.parent,
factID: v7(),
permission_set: entity_set.set,
type: "text",
position: generateKeyBetween(props.position, props.nextPosition),
newEntityID: entity,
});
}
await rep.mutate.assertFact({
entity,
attribute: "block/type",
data: { type: "block-type-union", value: "image" },
});
await addImage(file, rep, {
entityID: entity,
attribute: "block/image",
});
});
};
if (!image) {
if (!entity_set.permissions.write) return null;
return (
);
}
let localSrc = localImages.get(image.data.src);
let blockClassName = `
relative group/image border-transparent! p-0! w-fit!
${isFullBleed && "-mx-[14px] sm:-mx-[18px] rounded-[0px]! sm:outline-offset-[-16px]! -outline-offset[-12px]!"}
${isFullBleed ? (isFirst ? "-mt-3 sm:-mt-4" : prevIsFullBleed ? "-mt-[5px]" : "") : ""}
${isFullBleed ? (isLast ? "-mb-4" : nextIsFullBleed ? "-mb-[9px]" : "") : ""}
`;
return (
{!props.preview && (
setLightboxOpen(i !== null)}
renderSlide={() => }
/>
)}
{!props.preview ? (
) : null}
{!props.preview ? : null}
);
}
const CoverImageButton = (props: { entityID: string }) => {
let { rep, rootEntity } = useReplicache();
let entity_set = useEntitySetContext();
let { data: pubData } = useLeafletPublicationData();
let publicationPage = useLeafletPublicationPage();
let image = useEntity(props.entityID, "block/image");
let alt = useEntity(props.entityID, "image/alt")?.data.value;
let coverEntity = useEntity(rootEntity, "root/cover-image")?.data.value;
let coverImage = useEntity(coverEntity ?? null, "block/image");
let isFocused = useUIState(
(s) => s.focusedEntity?.entityID === props.entityID,
);
// Only show if focused, in a publication, with write permissions, and an
// image to copy. Publication pages (e.g. an About page) have no cover image.
if (
!isFocused ||
!pubData?.publications ||
!entity_set.permissions.write ||
publicationPage ||
!image
)
return null;
// The cover is a standalone copy sharing this block's src, so a matching src
// means this block is the current cover.
let isCoverImage = !!coverImage && coverImage.data.src === image.data.src;
if (isCoverImage)
return (
{
e.preventDefault();
e.stopPropagation();
if (coverEntity)
await rep?.mutate.deleteEntity({ entity: coverEntity });
}}
>
Remove Cover Image
);
return (
{
e.preventDefault();
e.stopPropagation();
if (!rep) return;
await setCoverImageFromEntity(rep, {
rootEntity,
permission_set: entity_set.set,
image: image.data,
alt,
});
}}
>
Use as Cover Image
);
};