Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
7.7 kB · 231 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232import { useEntitySetContext } from "components/EntitySetProvider";import { generateKeyBetween } from "fractional-indexing";import { useEffect, useState } from "react";import { useEntity, useReplicache } from "src/replicache";import { useUIState } from "src/useUIState";import { addLinkBlock } from "src/utils/addLinkBlock";import { BlockProps, BlockLayout } from "./Block";import { v7 } from "uuid";import { useSmoker } from "components/Toast";import { Separator } from "components/Layout";import { Input } from "components/Input";import { focusElement } from "src/utils/focusElement";import { isUrl } from "src/utils/isURL";import { elementId } from "src/utils/elementId";import { focusBlock } from "src/utils/focusBlock";import { CheckTiny } from "components/Icons/CheckTiny";import { LinkSmall } from "components/Icons/LinkSmall";
export const ExternalLinkBlock = ( props: BlockProps & { preview?: boolean },) => { let { permissions } = useEntitySetContext(); let previewImage = useEntity(props.entityID, "link/preview"); let title = useEntity(props.entityID, "link/title"); let description = useEntity(props.entityID, "link/description"); let url = useEntity(props.entityID, "link/url");
let isSelected = useUIState((s) => s.selectedBlocks.find((b) => b.value === props.entityID), ); useEffect(() => { if (props.preview) return; let input = document.getElementById(elementId.block(props.entityID).input); if (isSelected) { setTimeout(() => { let input = document.getElementById( elementId.block(props.entityID).input, ); focusElement(input as HTMLInputElement | null); }, 20); } else input?.blur(); }, [isSelected, props.entityID, props.preview]);
if (url === undefined) { if (!permissions.write) return null; return ( <label className={` w-full h-[104px] p-2 text-tertiary hover:text-accent-contrast hover:cursor-pointer flex flex-auto gap-2 items-center justify-center hover:border-2 border-dashed rounded-lg ${isSelected ? "border-2 border-tertiary" : "border border-border"} ${props.pageType === "canvas" && "bg-bg-page"}`} onMouseDown={() => { focusBlock( { type: props.type, value: props.entityID, parent: props.parent }, { type: "start" }, ); }} > <BlockLinkInput {...props} /> </label> ); }
return ( <BlockLayout isSelected={!!isSelected} hasBackground="page" borderOnHover className="externalLinkBlock flex relative group/linkBlock h-[104px] p-0!" > <a href={url?.data.value} target="_blank" referrerPolicy="no-referrer" className="flex w-full h-full text-primary hover:no-underline no-underline" > <div className="pt-2 pb-2 px-3 grow min-w-0"> <div className="flex flex-col w-full min-w-0 h-full grow "> <div className={`linkBlockTitle bg-transparent -mb-0.5 border-none font-bold outline-hidden resize-none align-top border h-[24px] line-clamp-1`} style={{ overflow: "hidden", textOverflow: "ellipsis", wordBreak: "break-all", }} > {title?.data.value} </div>
<div className={`linkBlockDescription text-sm bg-transparent border-none outline-hidden resize-none align-top grow line-clamp-2`} > {description?.data.value} </div> <div style={{ wordBreak: "break-word" }} // better than tailwind break-all! className={`min-w-0 w-full line-clamp-1 text-xs italic group-hover/linkBlock:text-accent-contrast ${isSelected ? "text-accent-contrast" : "text-tertiary"}`} > {url?.data.value} </div> </div> </div>
<div className={`linkBlockPreview w-[120px] m-2 -mb-2 bg-cover shrink-0 rounded-t-md border border-border rotate-[4deg] origin-center`} style={{ backgroundImage: `url(${previewImage?.data.src})`, backgroundPosition: "center", }} /> </a> </BlockLayout> );};
const BlockLinkInput = (props: BlockProps & { preview?: boolean }) => { let isSelected = useUIState((s) => s.selectedBlocks.find((b) => b.value === props.entityID), ); let entity_set = useEntitySetContext(); let [linkValue, setLinkValue] = useState(""); let { rep } = useReplicache(); let submit = async () => { let linkEntity = props.entityID; if (!linkEntity) { linkEntity = v7();
await rep?.mutate.addBlock({ permission_set: entity_set.set, factID: v7(), parent: props.parent, type: "card", position: generateKeyBetween(props.position, props.nextPosition), newEntityID: linkEntity, }); } let link = linkValue; if (!linkValue.startsWith("http")) link = `https://${linkValue}`; addLinkBlock(link, linkEntity, rep);
let textEntity = v7(); await rep?.mutate.addBlock({ permission_set: entity_set.set, factID: v7(), parent: props.parent, type: "text", position: generateKeyBetween(props.position, props.nextPosition), newEntityID: textEntity, });
focusBlock( { value: textEntity, type: "text", parent: props.parent, }, { type: "start" }, ); }; let smoker = useSmoker();
return ( <div className={`max-w-sm flex gap-2 rounded-md text-secondary`}> <> <LinkSmall className={`shrink-0 ${isSelected ? "text-tertiary" : "text-border"} `} /> <Separator /> <Input id={ !props.preview ? elementId.block(props.entityID).input : undefined } type="url" className="w-full grow border-none outline-hidden bg-transparent " placeholder="www.example.com" value={linkValue} onChange={(e) => setLinkValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); if (!linkValue) return; if (!isUrl(linkValue)) { let rect = e.currentTarget.getBoundingClientRect(); smoker({ alignOnMobile: "left", error: true, text: "invalid url!", position: { x: rect.left, y: rect.top - 8 }, }); return; } submit(); } }} /> <div className="flex items-center gap-3 "> <button autoFocus={false} className={`p-1 ${isSelected ? "text-accent-contrast" : "text-border"}`} onMouseDown={(e) => { e.preventDefault(); if (!linkValue || linkValue === "") { smoker({ alignOnMobile: "left", error: true, text: "no url!", position: { x: e.clientX, y: e.clientY }, }); return; } if (!isUrl(linkValue)) { smoker({ alignOnMobile: "left", error: true, text: "invalid url!", position: { x: e.clientX, y: e.clientY }, }); return; } submit(); }} > <CheckTiny /> </button> </div> </> </div> );};