"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 ); };