diff --git a/apps/web/core/constants.ts b/apps/web/core/constants.ts index 982cfef6be..694996bcb1 100644 --- a/apps/web/core/constants.ts +++ b/apps/web/core/constants.ts @@ -60,6 +60,8 @@ export const ADDRESS = '5c6e72fb834047c082818be159ecd495'; export const VENUE_PROPERTY = 'f28bbb6bf4e8465d9de7a09085e224b9'; export const ADDRESS_PROPERTY = '72ba2a0f729d4847925df3b09d46bb66'; export const MAPBOX_PROPERTY = 'f9d8ff4e52f14e73b8f2bbb31dce5465'; +export const PDF_TYPE = '14a39e59-d987-4596-956a-c2dd4165c210'; +export const PDF_URL = '9d69ac47-7023-43e2-b72a-91a335eb54d9'; export const UNIT_PROPERTY = '11b0658120d341eab5702ef4ee0a4ffd'; export const DEFAULT_DATE_FORMAT = 'MMMM d, yyyy'; export const DEFAULT_DATETIME_FORMAT = 'h:mmaaa, MMMM d, yyyy'; diff --git a/apps/web/core/io/dto/properties.ts b/apps/web/core/io/dto/properties.ts index e85af7a2e5..3020e1495f 100644 --- a/apps/web/core/io/dto/properties.ts +++ b/apps/web/core/io/dto/properties.ts @@ -1,6 +1,6 @@ import { SystemIds } from '@geoprotocol/geo-sdk'; -import { GEO_LOCATION, PLACE, VIDEO_RENDERABLE_TYPE } from '~/core/constants'; +import { GEO_LOCATION, PDF_TYPE, VIDEO_RENDERABLE_TYPE,PLACE } from '~/core/constants'; import { DataType as AppDataType, Property, RenderableType } from '~/core/types'; import { getDataTypeFromEntityId } from '~/core/utils/property/properties'; @@ -39,6 +39,8 @@ export function getStrictRenderableType(renderableType: string | null): Renderab return 'URL'; case GEO_LOCATION: return 'GEO_LOCATION'; + case PDF_TYPE: + return 'PDF'; case PLACE: return 'PLACE'; default: diff --git a/apps/web/core/state/editor/types.ts b/apps/web/core/state/editor/types.ts index c1201a30c9..95c0372a54 100644 --- a/apps/web/core/state/editor/types.ts +++ b/apps/web/core/state/editor/types.ts @@ -38,6 +38,14 @@ export type Content = id: string; src: string; }; + } + | { + type: 'pdf'; + content: Content[]; + attrs: { + id: string; + src: string; + }; }; type Mark = { diff --git a/apps/web/core/state/editor/use-editor.tsx b/apps/web/core/state/editor/use-editor.tsx index 08532841d9..5b4f6cf1ec 100644 --- a/apps/web/core/state/editor/use-editor.tsx +++ b/apps/web/core/state/editor/use-editor.tsx @@ -8,6 +8,7 @@ import { useSearchParams } from 'next/navigation'; import * as React from 'react'; +import { PDF_TYPE } from '~/core/constants'; import { storage } from '~/core/sync/use-mutate'; import { getRelations, getValues, useValues } from '~/core/sync/use-store'; import { Relation, RenderableEntityType, Value } from '~/core/types'; @@ -99,6 +100,8 @@ function makeNewBlockRelation({ return 'IMAGE'; case 'video': return 'VIDEO'; + case 'pdf': + return 'PDF'; } })(); @@ -319,6 +322,20 @@ export function useEditorStore() { }; } + if (toEntity?.type === 'PDF') { + return { + type: 'pdf', + attrs: { + id: block.block.id, + src: getImagePath(toEntity.value), + relationId: block.relationId, + spaceId, + alt: '', + title: '', + }, + }; + } + if (toEntity?.type === 'VIDEO') { // Read video URL from Values using IMAGE_URL_PROPERTY (unified IPFS URL property) const videoUrlValues = getValues({ @@ -449,6 +466,8 @@ export function useEditorStore() { return SystemIds.IMAGE_TYPE; case 'video': return SystemIds.VIDEO_TYPE; + case 'pdf': + return PDF_TYPE; default: return SystemIds.TEXT_BLOCK; } diff --git a/apps/web/core/sync/use-mutate.tsx b/apps/web/core/sync/use-mutate.tsx index add2d4c611..073b33eba0 100644 --- a/apps/web/core/sync/use-mutate.tsx +++ b/apps/web/core/sync/use-mutate.tsx @@ -1,7 +1,7 @@ import { Graph, Position, SystemIds } from '@geoprotocol/geo-sdk'; import { Draft, produce } from 'immer'; -import { DATA_TYPE_ENTITY_IDS, DATA_TYPE_PROPERTY, RENDERABLE_TYPE_PROPERTY } from '../constants'; +import { DATA_TYPE_ENTITY_IDS, DATA_TYPE_PROPERTY, PDF_TYPE, PDF_URL, RENDERABLE_TYPE_PROPERTY } from '../constants'; import { ID } from '../id'; import { OmitStrict } from '../types'; import { DataType, Relation, Value } from '../types'; @@ -97,6 +97,16 @@ export interface Mutator { spaceId: string; }) => Promise<{ videoId: string; relationId: string }>; }; + pdfs: { + createAndLink: (params: { + file: File; + fromEntityId: string; + fromEntityName?: string | null; + relationPropertyId: string; + relationPropertyName: string | null; + spaceId: string; + }) => Promise<{ imageId: string; relationId: string }>; + }; setAsPublished: (valueIds: string[], relationIds: string[]) => void; } @@ -169,9 +179,7 @@ function createMutator(store: GeoStore): Mutator { // Create the data type relation (every property gets an explicit Data Type relation). const dataTypeEntityId = DATA_TYPE_ENTITY_IDS[dataType]; if (dataTypeEntityId) { - const hasDataTypeRelation = existingRelations.some( - r => r.type.id === DATA_TYPE_PROPERTY && !r.isDeleted - ); + const hasDataTypeRelation = existingRelations.some(r => r.type.id === DATA_TYPE_PROPERTY && !r.isDeleted); if (!hasDataTypeRelation) { store.setRelation({ @@ -205,10 +213,7 @@ function createMutator(store: GeoStore): Mutator { // we don't need to create the property type relation again if (!skipTypeRelation) { const hasTypesRelation = existingRelations.some( - r => - r.type.id === SystemIds.TYPES_PROPERTY && - r.toEntity.id === SystemIds.PROPERTY && - !r.isDeleted + r => r.type.id === SystemIds.TYPES_PROPERTY && r.toEntity.id === SystemIds.PROPERTY && !r.isDeleted ); if (!hasTypesRelation) { @@ -422,7 +427,7 @@ function createMutator(store: GeoStore): Mutator { let ipfsUrl: string | undefined; for (const op of createVideoOps) { if (op.type === 'createEntity') { - const ipfsValue = op.values.find(pv => { + const ipfsValue = op.values.find((pv: any) => { const valStr = extractValueString(pv.value); return valStr.startsWith('ipfs://'); }); @@ -507,6 +512,98 @@ function createMutator(store: GeoStore): Mutator { return { videoId: videoIdStr, relationId }; }, }, + pdfs: { + createAndLink: async ({ + file, + fromEntityId, + fromEntityName, + relationPropertyId, + relationPropertyName, + spaceId, + }) => { + // Create the image entity using the Graph API + // Use TESTNET network to upload to Pinata via alternative gateway + const { id: pdfId, ops: createImageOps } = await Graph.createImage({ + blob: file, + network: 'TESTNET', + }); + + // Process the operations returned by Graph.createImage + for (const op of createImageOps) { + if (op.type === 'createRelation') { + store.setRelation({ + id: toHexId(op.id), + entityId: op.entity ? toHexId(op.entity) : toHexId(op.from), + fromEntity: { + id: toHexId(op.from), + name: null, + }, + type: { + id: toHexId(op.relationType), + name: 'PDF', + }, + toEntity: { + id: toHexId(op.to), + name: 'PDF', + value: toHexId(op.to), + }, + spaceId, + position: Position.generate(), + verified: false, + renderableType: 'RELATION', + }); + } else if (op.type === 'createEntity') { + for (const pv of op.values) { + store.setValue({ + id: ID.createValueId({ + entityId: toHexId(op.id), + propertyId: toHexId(pv.property), + spaceId, + }), + entity: { + id: toHexId(op.id), + name: null, + }, + property: { + id: PDF_URL, + name: 'PDF Property', + dataType: 'TEXT', + renderableType: 'URL', + }, + spaceId, + value: extractValueString(pv.value), + }); + } + } + } + + // Create relation from parent entity to pdf entity + const relationId = ID.createEntityId(); + store.setRelation({ + id: relationId, + entityId: ID.createEntityId(), + fromEntity: { + id: fromEntityId, + name: fromEntityName || '', + }, + type: { + id: relationPropertyId, + name: relationPropertyName || '', + }, + toEntity: { + id: pdfId, + name: null, + value: pdfId, + }, + spaceId, + position: Position.generate(), + verified: false, + renderableType: 'PDF', + }); + + return { imageId: pdfId, relationId }; + }, + }, setAsPublished: (valueIds, relationIds) => { store.setAsPublished(valueIds, relationIds); }, diff --git a/apps/web/core/types.ts b/apps/web/core/types.ts index e960b819c6..79b66c9d9c 100644 --- a/apps/web/core/types.ts +++ b/apps/web/core/types.ts @@ -86,10 +86,10 @@ export type DataType = // Renderable Types // ============================================================================== -export type RenderableType = 'IMAGE' | 'VIDEO' | 'URL' | 'GEO_LOCATION' | 'PLACE'; // GEO_LOCATION needs to be migrated to SDK +export type RenderableType = 'IMAGE' | 'VIDEO' | 'URL' | 'GEO_LOCATION' | 'PLACE' | 'PDF'; // GEO_LOCATION needs to be migrated to SDK export type RawRenderableType = string; // UUIDs of renderable types -export type RenderableEntityType = 'IMAGE' | 'VIDEO' | 'RELATION' | 'DATA' | 'TEXT' | 'POINT'; +export type RenderableEntityType = 'IMAGE' | 'VIDEO' | 'RELATION' | 'DATA' | 'TEXT' | 'POINT' | 'PDF'; // Renderable fields are a special data model to represent us rendering both // triples and relations in the same way. This is used across tables and entity @@ -135,7 +135,7 @@ export type SwitchableRenderableType = | 'POINT' | 'GEO_LOCATION' | 'PLACE' - | 'ADDRESS'; + | 'ADDRESS' | 'PDF'; /** * Human-readable labels for switchable renderable types @@ -157,6 +157,7 @@ export const SWITCHABLE_RENDERABLE_TYPE_LABELS: Record ({ FORMAT_PROPERTY: 'FORMAT_PROPERTY_ID', UNIT_PROPERTY: 'UNIT_PROPERTY_ID', VIDEO_RENDERABLE_TYPE: 'VIDEO_RENDERABLE_TYPE_ID', + PDF_TYPE: 'PDF_TYPE_ID', PLACE: 'PLACE_ID', })); diff --git a/apps/web/core/utils/property/properties.ts b/apps/web/core/utils/property/properties.ts index 57fdfaa676..97a6d8d114 100644 --- a/apps/web/core/utils/property/properties.ts +++ b/apps/web/core/utils/property/properties.ts @@ -6,6 +6,7 @@ import { DATA_TYPE_PROPERTY, FORMAT_PROPERTY, GEO_LOCATION, + PDF_TYPE, PLACE, RENDERABLE_TYPE_PROPERTY, UNIT_PROPERTY, @@ -130,6 +131,11 @@ export function mapPropertyType(type: SwitchableRenderableType): PropertyTypeMap baseDataType: 'RELATION', renderableTypeId: PLACE, }; + case 'PDF': + return { + baseDataType: 'RELATION', + renderableTypeId: PDF_TYPE + } case 'ADDRESS': return { baseDataType: 'RELATION', @@ -169,6 +175,7 @@ export const typeToBaseDataType: Record = { POINT: 'POINT', GEO_LOCATION: 'POINT', PLACE: 'RELATION', + PDF: 'RELATION', ADDRESS: 'RELATION', } as const; diff --git a/apps/web/core/utils/utils.ts b/apps/web/core/utils/utils.ts index 8913bf9cd7..e4c518d6cc 100644 --- a/apps/web/core/utils/utils.ts +++ b/apps/web/core/utils/utils.ts @@ -401,10 +401,10 @@ export const getImageHash = (value: string) => { // e.g., ipfs://HASH -> https://example.mypinata.cloud/files/HASH export const getImagePath = (value: string) => { // Use Pinata gateway as the primary source for IPFS images - if (value.startsWith('ipfs://')) { + if (value?.startsWith('ipfs://')) { return `${PINATA_GATEWAY_READ_PATH}${getImageHash(value)}`; // The image likely resolves to an image resource at some URL - } else if (value.startsWith('http')) { + } else if (value?.startsWith('http')) { return value; } else { // The image is likely a static, bundled path diff --git a/apps/web/design-system/chip.tsx b/apps/web/design-system/chip.tsx index 78bd648991..ba1e4302cd 100644 --- a/apps/web/design-system/chip.tsx +++ b/apps/web/design-system/chip.tsx @@ -2,6 +2,7 @@ import * as Popover from '@radix-ui/react-popover'; import { cva } from 'class-variance-authority'; +import dynamic from 'next/dynamic'; import * as React from 'react'; import { useState } from 'react'; @@ -24,6 +25,10 @@ import { PrefetchLink as Link } from '~/design-system/prefetch-link'; import { SelectSpaceAsPopover } from '~/design-system/select-space-dialog'; import { ColorName, colors } from '~/design-system/theme/colors'; +const PdfZoom = dynamic(() => import('./editable-fields/pdf-preview'), { + ssr: false, +}); + type LinkableChipProps = { href: string; children: React.ReactNode; @@ -318,7 +323,7 @@ function RelationDots({ color }: RelationDotsProps) { type LinkableMediaChipProps = { isEditing: boolean; - mediaType: 'IMAGE' | 'VIDEO'; + mediaType: 'IMAGE' | 'VIDEO' | 'PDF'; mediaSrc?: string; isUploading?: boolean; @@ -402,7 +407,7 @@ export function LinkableMediaChip({ href={NavUtils.toEntity(spaceId ?? currentSpaceId, entityId)} className="block" > -
+
{mediaType === 'VIDEO' ? ( videoSrc ? (
) - ) : mediaSrc ? ( + ) : mediaSrc && mediaType === 'IMAGE' ? ( + ) : mediaSrc && mediaType === 'PDF' ? ( + ) : (
@@ -429,7 +436,7 @@ export function LinkableMediaChip({ {/* Upload button overlay - only in edit mode */} - {isEditing && onUpload && !isUploading && ( + {isEditing && mediaType !== 'PDF' && onUpload && !isUploading && (
); } + +export function PdfField({ imageSrc, onFileChange, onImageRemove, variant = 'avatar' }: ImageFieldProps) { + const [isUploading, setIsUploading] = React.useState(false); + const fileInputRef = useRef(null); + const handleFileInputClick = () => { + // This is a hack to get around label htmlFor triggering a file input not working with nested React components. + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleChange = async (e: ChangeEvent) => { + if (e.target.files && onFileChange) { + const file = e.target.files[0]; + setIsUploading(true); + try { + await onFileChange(file); + } finally { + setIsUploading(false); + } + } + }; + + const cancelUploading = () => { + // TODO add cancel uploading logic + console.log('Cancel uploading PDF file'); + }; + + return ( +
+ {imageSrc && ( +
+ +
+ )} + {isUploading && } +
+ }> + Upload + + {imageSrc && } />} +
+ + +
+ ); +} diff --git a/apps/web/design-system/editable-fields/pdf-preview.tsx b/apps/web/design-system/editable-fields/pdf-preview.tsx new file mode 100644 index 0000000000..ce25dc3a8b --- /dev/null +++ b/apps/web/design-system/editable-fields/pdf-preview.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { Document, Page, pdfjs } from 'react-pdf'; +import 'react-pdf/dist/Page/AnnotationLayer.css'; +import 'react-pdf/dist/Page/TextLayer.css'; + +import { useState } from 'react'; + +import { useImageWithFallback } from '~/core/hooks/use-image-with-fallback'; + +import { PdfFile } from '../icons/file-pdf'; + +pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; + +export default function PdfZoom({ + pdfSrc, + isEditing = false, + className, + width, +}: { + pdfSrc: string; + isEditing?: boolean; + className?: string; + width?: number; +}) { + const { src } = useImageWithFallback(pdfSrc); + const [numPages, setNumPages] = useState(null); + const [fileName, setFileName] = useState(null); + + return ( +
+
+ { + setNumPages(pdf.numPages); + const metadata = (await pdf.getMetadata().catch(() => null)) as { + info?: { Title?: string }; + }; + + setFileName(metadata?.info?.Title ?? 'Document.pdf'); + }} + > + + +
+ {isEditing && ( +
+ {fileName} +
+ + + {numPages} page{numPages && numPages > 1 && 's'} + +
+
+ )} +
+ ); +} diff --git a/apps/web/design-system/icons/file-pdf.tsx b/apps/web/design-system/icons/file-pdf.tsx new file mode 100644 index 0000000000..6aaedcf8b5 --- /dev/null +++ b/apps/web/design-system/icons/file-pdf.tsx @@ -0,0 +1,23 @@ +export function PdfFile({ className, color }: { className?: string; color?: string }) { + return ( + + + + + ); +} diff --git a/apps/web/design-system/uploading-pdf-state.tsx b/apps/web/design-system/uploading-pdf-state.tsx new file mode 100644 index 0000000000..52118fcc24 --- /dev/null +++ b/apps/web/design-system/uploading-pdf-state.tsx @@ -0,0 +1,13 @@ +export const UploadingPdfState = ({ cancelUploading }: { cancelUploading: () => void }) => { + return ( +
+ Uploading +
+
+
+ +
+ ); +}; diff --git a/apps/web/package.json b/apps/web/package.json index 34bb899b8c..4aebcddb50 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,6 +34,8 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.7", "@radix-ui/react-use-rect": "^1.1.0", + "@react-pdf-viewer/core": "^3.12.0", + "@react-pdf-viewer/default-layout": "^3.12.0", "@tanstack/pacer": "^0.9.1", "@tanstack/react-query": "^5.90.20", "@tanstack/react-query-devtools": "^5.91.3", @@ -91,6 +93,7 @@ "react-error-boundary": "^4.0.10", "react-input-autosize": "^3.0.0", "react-medium-image-zoom": "^5.2.13", + "react-pdf": "^10.3.0", "react-remove-scroll": "^2.7.2", "react-textarea-autosize": "8.5.9", "react-use-measure": "^2.1.1", diff --git a/apps/web/partials/editor/command-items.tsx b/apps/web/partials/editor/command-items.tsx index 4fff67a97a..7043620393 100644 --- a/apps/web/partials/editor/command-items.tsx +++ b/apps/web/partials/editor/command-items.tsx @@ -1,7 +1,11 @@ +import { Graph } from '@geoprotocol/geo-sdk'; import { Editor, Range } from '@tiptap/core'; import * as React from 'react'; +import { getImagePath } from '~/core/utils/utils'; +import { extractValueString } from '~/core/utils/value'; + import { EditorH1 } from '~/design-system/icons/editor-h1'; import { EditorH2 } from '~/design-system/icons/editor-h2'; import { EditorH3 } from '~/design-system/icons/editor-h3'; @@ -10,6 +14,7 @@ import { EditorList } from '~/design-system/icons/editor-list'; import { EditorTable } from '~/design-system/icons/editor-table'; import { EditorText } from '~/design-system/icons/editor-text'; import { EditorVideo } from '~/design-system/icons/editor-video'; +import { PdfFile } from '~/design-system/icons/file-pdf'; export interface CommandSuggestionItem { title: string; @@ -90,6 +95,55 @@ export const commandItems: CommandSuggestionItem[] = [ editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run(); }, }, + { + icon: , + title: 'PDF', + command: async ({ editor, range }) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'application/pdf'; + input.className = 'hidden'; + input.onchange = async (e: any) => { + if (!e?.target?.files?.[0]) return; + const file = e.target.files[0]; + // Use TESTNET network to upload to Pinata via alternative gateway + const { id, ops } = await Graph.createImage({ + blob: file, + network: 'TESTNET', + }); + + // Extract the pdf URL from the ops - look for createEntity with ipfs:// value + let ipfsUrl: string | undefined; + for (const op of ops) { + if (op.type === 'createEntity') { + const ipfsValue = op.values.find((v: any) => v.value.value.startsWith('ipfs://')); + + if (ipfsValue) { + ipfsUrl = extractValueString(ipfsValue.value); + break; + } + } + } + + const src = ipfsUrl ? getImagePath(ipfsUrl) : ''; + + if (src) { + editor + .chain() + .focus() + .deleteRange(range) + .insertContent({ + type: 'pdf', + attrs: { + src, + }, + }) + .run(); + } + }; + input.click(); + }, + }, { icon: , title: 'Image', diff --git a/apps/web/partials/editor/extensions.tsx b/apps/web/partials/editor/extensions.tsx index ee0f1f4a6c..9faf351a59 100644 --- a/apps/web/partials/editor/extensions.tsx +++ b/apps/web/partials/editor/extensions.tsx @@ -15,6 +15,7 @@ import { DataNode } from './data-node'; import { HeadingNode } from './heading-node'; import { ImageNode } from './image-node'; import { ParagraphNode } from './paragraph-node'; +import { PdfNode } from './pdf-node'; import { TrailingNode } from './trailing-node'; import { VideoNode } from './video-node'; @@ -54,6 +55,7 @@ export const tiptapExtensions = [ DataNode, ImageNode, VideoNode, + PdfNode, Placeholder.configure({ placeholder: ({ node }) => { const isHeading = node.type.name === 'heading'; diff --git a/apps/web/partials/editor/pdf-node.tsx b/apps/web/partials/editor/pdf-node.tsx new file mode 100644 index 0000000000..d5f47a471d --- /dev/null +++ b/apps/web/partials/editor/pdf-node.tsx @@ -0,0 +1,60 @@ +import { Node, NodeViewProps, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react'; +import dynamic from 'next/dynamic'; + +import { useHydrateEntity } from '~/core/sync/use-store'; + +const PdfZoom = dynamic(() => import('../../design-system/editable-fields/pdf-preview'), { + ssr: false, +}); + +export const PdfNode = Node.create({ + name: 'pdf', + group: 'block', + atom: true, + spanning: false, + allowGapCursor: false, + defining: true, + exitable: true, + + addAttributes() { + return { + src: { + default: '', + }, + }; + }, + + parseHTML() { + return [{ tag: 'pdf-node' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['pdf-node', mergeAttributes(HTMLAttributes), 0]; + }, + + addNodeView() { + return ReactNodeViewRenderer(PdfNodeComponent); + }, +}); + +function PdfNodeComponent({ node }: NodeViewProps) { + const { id, src } = node.attrs; + + return ( + +
+ +
+
+ ); +} + +function PdfNodeChildren({ pdfSrc, entityId }: { pdfSrc: string; entityId: string }) { + useHydrateEntity({ id: entityId }); + + return ( +
+ +
+ ); +} diff --git a/apps/web/partials/entity-page/data-type-pill.tsx b/apps/web/partials/entity-page/data-type-pill.tsx index f922d76394..93ab50efc1 100644 --- a/apps/web/partials/entity-page/data-type-pill.tsx +++ b/apps/web/partials/entity-page/data-type-pill.tsx @@ -1,6 +1,5 @@ 'use client'; -import * as React from 'react'; import { DataType, RawRenderableType, SWITCHABLE_RENDERABLE_TYPE_LABELS, SwitchableRenderableType } from '~/core/types'; import { NavUtils } from '~/core/utils/utils'; diff --git a/apps/web/partials/entity-page/editable-entity-page.tsx b/apps/web/partials/entity-page/editable-entity-page.tsx index 69e221b6c7..3a87a99485 100644 --- a/apps/web/partials/entity-page/editable-entity-page.tsx +++ b/apps/web/partials/entity-page/editable-entity-page.tsx @@ -10,6 +10,7 @@ import { DATA_TYPE_PROPERTY, FORMAT_PROPERTY, IS_TYPE_PROPERTY, + PDF_TYPE, RENDERABLE_TYPE_PROPERTY, VALUE_TYPE_PROPERTY, } from '~/core/constants'; @@ -33,20 +34,23 @@ import { isUrlTemplate, resolveUrlTemplate } from '~/core/utils/url-template'; import { NavUtils } from '~/core/utils/utils'; import { AddTypeButton, SquareButton } from '~/design-system/button'; +import { SmallButton } from '~/design-system/button'; import { Checkbox, getChecked } from '~/design-system/checkbox'; -import { LinkableMediaChip, LinkableRelationChip } from '~/design-system/chip'; +import { LinkableMediaChip } from '~/design-system/chip'; import { DateField } from '~/design-system/editable-fields/date-field'; import { ImageZoom, PageImageField, PageStringField, PageVideoField, + PdfField, VideoPlayer, } from '~/design-system/editable-fields/editable-fields'; import { GeoLocationPointFields, GeoLocationWrapper } from '~/design-system/editable-fields/geo-location-field'; import { NumberField } from '~/design-system/editable-fields/number-field'; import { Create } from '~/design-system/icons/create'; import { Trash } from '~/design-system/icons/trash'; +import { Upload } from '~/design-system/icons/upload'; import { InputPlace } from '~/design-system/input-address'; import { PrefetchLink as Link } from '~/design-system/prefetch-link'; import ReorderableRelationChipsDnd from '~/design-system/reorderable-relation-chips-dnd'; @@ -271,7 +275,7 @@ export function RelationsGroup({ propertyId, id, spaceId }: RelationsGroupProps) }); // For IMAGE properties, get the image URL from related image entities - const imageRelation = relations.find(r => r.renderableType === 'IMAGE'); + const imageRelation = relations.find(r => r.renderableType === 'IMAGE' || r.renderableType === 'PDF'); const imageEntityId = imageRelation?.toEntity.id; // Use the efficient hook to get only the image URL for this specific entity @@ -303,7 +307,7 @@ export function RelationsGroup({ propertyId, id, spaceId }: RelationsGroupProps) const valueType = relationValueTypes?.[0]; const isEmpty = relations.length === 0; - // Handler for file upload (images and videos) + // Handler for file upload (images/videos/pdfs) const handleFileUpload = async (file: File) => { setIsUploading(true); try { @@ -316,7 +320,7 @@ export function RelationsGroup({ propertyId, id, spaceId }: RelationsGroupProps) relationPropertyName: typeOfName, spaceId, }); - } else { + } else if (property.renderableTypeStrict === 'IMAGE') { await storage.images.createAndLink({ file, fromEntityId: id, @@ -325,6 +329,15 @@ export function RelationsGroup({ propertyId, id, spaceId }: RelationsGroupProps) relationPropertyName: typeOfName, spaceId, }); + } else if (property.renderableTypeStrict === 'PDF') { + await storage.pdfs.createAndLink({ + file, + fromEntityId: id, + fromEntityName: name, + relationPropertyId: propertyId, + relationPropertyName: typeOfName, + spaceId, + }); } } finally { setIsUploading(false); @@ -345,7 +358,7 @@ export function RelationsGroup({ propertyId, id, spaceId }: RelationsGroupProps) if (isEmpty) { return ( -
+
{property.renderableTypeStrict === 'IMAGE' ? (
+ ) : property.renderableTypeStrict === 'PDF' ? ( +
+ { + await storage.pdfs.createAndLink({ + file, + fromEntityId: id, + fromEntityName: name, + relationPropertyId: propertyId, + relationPropertyName: typeOfName, + spaceId, + }); + }} + onImageRemove={() => console.log(`remove`)} + /> +
) : propertyId === VENUE_PROPERTY ? (
+
{/* Hidden file input for upload */} - {property.renderableTypeStrict === 'IMAGE' ? ( - relations.map(r => { + {property.renderableTypeStrict === 'IMAGE' || property.renderableTypeStrict === 'PDF' ? ( + relations.map((r, index) => { const relationId = r.id; const relationValue = r.toEntity.id; - return ( - storage.relations.delete(r)} - onDone={result => { - storage.relations.update(r, draft => { - draft.toSpaceId = result.space; - draft.verified = result.verified; - }); - }} - onUpload={triggerFileUpload} - /> - ); + if (property.renderableTypeStrict === 'IMAGE') { + return ( + storage.relations.delete(r)} + onDone={result => { + storage.relations.update(r, draft => { + draft.toSpaceId = result.space; + draft.verified = result.verified; + }); + }} + onUpload={triggerFileUpload} + /> + ); + } else { + return ( +
+ storage.relations.delete(r)} + onDone={result => { + storage.relations.update(r, draft => { + draft.toSpaceId = result.space; + draft.verified = result.verified; + }); + }} + onUpload={triggerFileUpload} + /> +
+ ); + } }) ) : property.renderableTypeStrict === 'VIDEO' ? ( relations.map(r => { @@ -613,37 +667,21 @@ export function RelationsGroup({ propertyId, id, spaceId }: RelationsGroupProps) /> )} - {property.renderableType !== SystemIds.IMAGE && property.renderableTypeStrict !== 'VIDEO' && ( -
- } label="type" /> - ) : ( - } /> - ) - } - placeholder={isType ? 'Find or create type...' : undefined} - relationValueTypes={relationValueTypes ? relationValueTypes : undefined} - onCreateEntity={result => { - // Check if we're creating a Property entity - const isCreatingProperty = valueType?.id === SystemIds.PROPERTY; - - if (isCreatingProperty) { - // Use the proper property creation flow which sets dataType correctly - const renderableType = result.renderableType || 'TEXT'; - const { baseDataType, renderableTypeId } = mapPropertyType(renderableType); - storage.properties.create({ - entityId: result.id, - spaceId, - name: result.name ?? '', - dataType: baseDataType, - renderableTypeId, - verified: result.verified, - toSpaceId: result.space, - }); - } else { - // Standard entity creation + {property.renderableType !== SystemIds.IMAGE && + property.renderableTypeStrict !== 'VIDEO' && + property.renderableType !== PDF_TYPE && ( +
+ } label="type" /> + ) : ( + } /> + ) + } + placeholder={isType ? 'Find or create type...' : undefined} + relationValueTypes={relationValueTypes ? relationValueTypes : undefined} + onCreateEntity={result => { storage.values.set({ id: ID.createValueId({ entityId: result.id, @@ -685,51 +723,50 @@ export function RelationsGroup({ propertyId, id, spaceId }: RelationsGroupProps) }, }); } - } - }} - onDone={async result => { - const newRelationId = ID.createEntityId(); - // @TODO(migration): lightweight relation pointing to entity id - const newEntityId = ID.createEntityId(); - - const newRelation: Relation = { - id: newRelationId, - spaceId: spaceId, - position: Position.generate(), - renderableType: 'RELATION', - verified: false, - entityId: newEntityId, - type: { - id: typeOfId, - name: typeOfName, - }, - fromEntity: { - id: id, - name: name, - }, - toEntity: { - id: result.id, - name: result.name, - value: result.id, - }, - }; - - if (result.space) { - newRelation.toSpaceId = result.space; - } + }} + onDone={async result => { + const newRelationId = ID.createEntityId(); + // @TODO(migration): lightweight relation pointing to entity id + const newEntityId = ID.createEntityId(); - if (result.verified) { - newRelation.verified = true; - } + const newRelation: Relation = { + id: newRelationId, + spaceId: spaceId, + position: Position.generate(), + renderableType: 'RELATION', + verified: false, + entityId: newEntityId, + type: { + id: typeOfId, + name: typeOfName, + }, + fromEntity: { + id: id, + name: name, + }, + toEntity: { + id: result.id, + name: result.name, + value: result.id, + }, + }; - storage.relations.set(newRelation); + if (result.space) { + newRelation.toSpaceId = result.space; + } - await applyTemplate({ ...templateOptions, propertyId: typeOfId, typeId: result.id }); - }} - spaceId={spaceId} - /> -
- )} + if (result.verified) { + newRelation.verified = true; + } + + storage.relations.set(newRelation); + + await applyTemplate({ ...templateOptions, propertyId: typeOfId, typeId: result.id }); + }} + spaceId={spaceId} + /> +
+ )} {/* Show geo location map for the first Address or Venue relation */} {shouldShowMap && firstRelation && ( @@ -789,6 +826,55 @@ function ImageRelationChipWrapper({ ); } +// Wrapper component for pdf relations in edit mode +function PdfRelationChipWrapper({ + relation, + spaceId, + isUploading, + onDelete, + onDone, + onUpload, + index, +}: { + relation: Relation; + spaceId: string; + isUploading?: boolean; + onDelete: () => void; + onDone: (result: { id: string; name: string | null; space?: string; verified?: boolean }) => void; + onUpload: () => void; + index: number; +}) { + const entityId = relation.toEntity.id; + const imageSrc = useImageUrlFromEntity(entityId, spaceId); + + return ( +
+ + {index === 0 && !isUploading && ( +
+ }> + Upload + +
+ )} +
+ ); +} + // Wrapper component for video relations in edit mode function VideoRelationChipWrapper({ relation, diff --git a/apps/web/partials/entity-page/readable-entity-page.tsx b/apps/web/partials/entity-page/readable-entity-page.tsx index 261a81e639..8d21101cff 100644 --- a/apps/web/partials/entity-page/readable-entity-page.tsx +++ b/apps/web/partials/entity-page/readable-entity-page.tsx @@ -1,6 +1,7 @@ 'use client'; import { ContentIds, SystemIds } from '@geoprotocol/geo-sdk'; +import dynamic from 'next/dynamic'; import * as React from 'react'; @@ -15,8 +16,8 @@ import { useValues, } from '~/core/sync/use-store'; import { DataType, RenderableType } from '~/core/types'; -import { useImageUrlFromEntity, useVideoUrlFromEntity } from '~/core/utils/use-entity-media'; import { isUrlTemplate } from '~/core/utils/url-template'; +import { useImageUrlFromEntity, useVideoUrlFromEntity } from '~/core/utils/use-entity-media'; import { GeoNumber, GeoPoint, NavUtils, sortRelations } from '~/core/utils/utils'; import { Checkbox, getChecked } from '~/design-system/checkbox'; @@ -29,6 +30,10 @@ import { Map } from '~/design-system/map'; import { PrefetchLink as Link } from '~/design-system/prefetch-link'; import { Text } from '~/design-system/text'; +const PdfZoom = dynamic(() => import('../../design-system/editable-fields/pdf-preview'), { + ssr: false, +}); + interface Props { id: string; spaceId: string; @@ -231,6 +236,17 @@ export function RelationsGroup({ ); } + if (property.renderableTypeStrict === 'PDF') { + return ( + + ); + } + return (
; } +function PdfRelation({ linkedEntityId, spaceId }: { linkedEntityId: string; relationId: string; spaceId: string }) { + const actualPdfSrc = useImageUrlFromEntity(linkedEntityId, spaceId); + + return ; +} + function RenderedValue({ entityId, propertyId, @@ -394,7 +416,15 @@ function RenderedValue({ case 'DATE': case 'DATETIME': case 'TIME': { - return ; + return ( + + ); } } } diff --git a/apps/web/partials/entity-page/type-icons.ts b/apps/web/partials/entity-page/type-icons.ts index 40c285c38f..af06827395 100644 --- a/apps/web/partials/entity-page/type-icons.ts +++ b/apps/web/partials/entity-page/type-icons.ts @@ -6,6 +6,7 @@ import { SWITCHABLE_RENDERABLE_TYPE_LABELS, SwitchableRenderableType } from '~/c import { Address } from '~/design-system/icons/address'; import { CheckboxChecked } from '~/design-system/icons/checkbox-checked'; import { Date } from '~/design-system/icons/date'; +import { PdfFile } from '~/design-system/icons/file-pdf'; import { GeoLocation } from '~/design-system/icons/geo-location'; import { Image } from '~/design-system/icons/image'; import { Number } from '~/design-system/icons/number'; @@ -37,6 +38,7 @@ export const TYPE_ICONS: Record = { GEO_LOCATION: GeoLocation, PLACE: Place, ADDRESS: Address, + PDF: PdfFile, }; const LABEL_TO_KEY: Record = Object.fromEntries( diff --git a/apps/web/styles/styles.css b/apps/web/styles/styles.css index 1c6675986a..5b90ca8d62 100644 --- a/apps/web/styles/styles.css +++ b/apps/web/styles/styles.css @@ -186,3 +186,14 @@ input::-ms-clear { [data-radix-popper-content-wrapper] { width: var(--radix-popper-anchor-width); } + +/* Multiline text truncation (2 lines): + Limits text to 2 lines and adds ellipsis when content overflows. + Uses -webkit-line-clamp for cross-browser support. +*/ +.clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} diff --git a/bun.lock b/bun.lock index f6533baf8c..80e4d556dc 100644 --- a/bun.lock +++ b/bun.lock @@ -37,6 +37,8 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.7", "@radix-ui/react-use-rect": "^1.1.0", + "@react-pdf-viewer/core": "^3.12.0", + "@react-pdf-viewer/default-layout": "^3.12.0", "@tanstack/pacer": "^0.9.1", "@tanstack/react-query": "^5.90.20", "@tanstack/react-query-devtools": "^5.91.3", @@ -94,6 +96,7 @@ "react-error-boundary": "^4.0.10", "react-input-autosize": "^3.0.0", "react-medium-image-zoom": "^5.2.13", + "react-pdf": "^10.3.0", "react-remove-scroll": "^2.7.2", "react-textarea-autosize": "8.5.9", "react-use-measure": "^2.1.1", @@ -603,6 +606,30 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + "@napi-rs/canvas": ["@napi-rs/canvas@0.1.94", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.94", "@napi-rs/canvas-darwin-arm64": "0.1.94", "@napi-rs/canvas-darwin-x64": "0.1.94", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.94", "@napi-rs/canvas-linux-arm64-gnu": "0.1.94", "@napi-rs/canvas-linux-arm64-musl": "0.1.94", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.94", "@napi-rs/canvas-linux-x64-gnu": "0.1.94", "@napi-rs/canvas-linux-x64-musl": "0.1.94", "@napi-rs/canvas-win32-arm64-msvc": "0.1.94", "@napi-rs/canvas-win32-x64-msvc": "0.1.94" } }, "sha512-8jBkvqynXNdQPNZjLJxB/Rp9PdnnMSHFBLzPmMc615nlt/O6w0ergBbkEDEOr8EbjL8nRQDpEklPx4pzD7zrbg=="], + + "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.94", "", { "os": "android", "cpu": "arm64" }, "sha512-YQ6K83RWNMQOtgpk1aIML97QTE3zxPmVCHTi5eA8Nss4+B9JZi5J7LHQr7B5oD7VwSfWd++xsPdUiJ1+frqsMg=="], + + "@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.94", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h1yl9XjqSrYZAbBUHCVLAhwd2knM8D8xt081Pv40KqNJXfeMmBrhG1SfroRymG2ak+pl42iQlWjFZ2Z8AWFdSw=="], + + "@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.94", "", { "os": "darwin", "cpu": "x64" }, "sha512-rkr/lrafbU0IIHebst+sQJf1HjdHvTMN0GGqWvw5OfaVS0K/sVxhNHtxi8oCfaRSvRE62aJZjWTcdc2ue/o6yw=="], + + "@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.94", "", { "os": "linux", "cpu": "arm" }, "sha512-q95TDo32YkTKdi+Sp2yQ2Npm7pmfKEruNoJ3RUIw1KvQQ9EHKL3fii/iuU60tnzP0W+c8BKN7BFstNFcm2KXCQ=="], + + "@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.94", "", { "os": "linux", "cpu": "arm64" }, "sha512-Je5/gKVybWAoIGyDOcJF1zYgBTKWkPIkfOgvCzrQcl8h7DiDvRvEY70EapA+NicGe4X3DW9VsCT34KZJnerShA=="], + + "@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.94", "", { "os": "linux", "cpu": "arm64" }, "sha512-9YleDDauDEZNsFnfz3HyZvp1LK1ECu8N2gDUg1wtL7uWLQv8dUbfVeFtp5HOdxht1o7LsWRmQeqeIbnD4EqE2A=="], + + "@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.94", "", { "os": "linux", "cpu": "none" }, "sha512-lQUy9Xvz7ch8+0AXq8RkioLD41iQ6EqdKFu5uV40BxkBDijB2SCm1jna/BRhqitQRSjwAk2KlLUxTjHChyfNGg=="], + + "@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.94", "", { "os": "linux", "cpu": "x64" }, "sha512-0IYgyuUaugHdWxXRhDQUCMxTou8kAHHmpIBFtbmdRlciPlfK7AYQW5agvUU1PghPc5Ja3Zzp5qZfiiLu36vIWQ=="], + + "@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.94", "", { "os": "linux", "cpu": "x64" }, "sha512-xuetfzzcflCIiBw2HJlOU4/+zTqhdxoe1BEcwdBsHAd/5wAQ4Pp+FGPi5g74gDvtcXQmTdEU3fLQvHc/j3wbxQ=="], + + "@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@0.1.94", "", { "os": "win32", "cpu": "arm64" }, "sha512-2F3p8wci4Q4vjbENlQtSibqFWxBdpzYk1c8Jh1mqqLE92rBKElG018dBJ6C8Dp49vE350Hmy5LrfdLgFKMG8sg=="], + + "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.94", "", { "os": "win32", "cpu": "x64" }, "sha512-hjwaIKMrQLoNiu3724octSGhDVKkBwJtMeQ3qUXOi+y60h2q6Sxq3+MM2za3V88+XQzzwn0DgG0Xo6v6gzV8kQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@next/bundle-analyzer": ["@next/bundle-analyzer@16.1.6", "", { "dependencies": { "webpack-bundle-analyzer": "4.10.1" } }, "sha512-ee2kagdTaeEWPlotgdTOqFHYcD3e2m2bbE3I9Rq2i6ABYi5OgopmtEUe8NM23viaYxLV2tDH/2nd5+qKoEr6cw=="], @@ -801,6 +828,42 @@ "@react-aria/utils": ["@react-aria/utils@3.32.0", "", { "dependencies": { "@react-aria/ssr": "^3.9.10", "@react-stately/flags": "^3.1.2", "@react-stately/utils": "^3.11.0", "@react-types/shared": "^3.32.1", "@swc/helpers": "^0.5.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-/7Rud06+HVBIlTwmwmJa2W8xVtgxgzm0+kLbuFooZRzKDON6hhozS1dOMR/YLMxyJOaYOTpImcP4vRR9gL1hEg=="], + "@react-pdf-viewer/attachment": ["@react-pdf-viewer/attachment@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-mhwrYJSIpCvHdERpLUotqhMgSjhtF+BTY1Yb9Fnzpcq3gLZP+Twp5Rynq21tCrVdDizPaVY7SKu400GkgdMfZw=="], + + "@react-pdf-viewer/bookmark": ["@react-pdf-viewer/bookmark@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-i7nEit8vIFMAES8RFGwprZ9cXOOZb9ZStPW6E6yuObJEXcvBj/ctsbBJGZxqUZOGklM0JoB7sjHyxAriHfe92A=="], + + "@react-pdf-viewer/core": ["@react-pdf-viewer/core@3.12.0", "", { "peerDependencies": { "pdfjs-dist": "^2.16.105 || ^3.0.279", "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-8MsdlQJ4jaw3GT+zpCHS33nwnvzpY0ED6DEahZg9WngG++A5RMhk8LSlxdHelwaFFHFiXBjmOaj2Kpxh50VQRg=="], + + "@react-pdf-viewer/default-layout": ["@react-pdf-viewer/default-layout@3.12.0", "", { "dependencies": { "@react-pdf-viewer/attachment": "3.12.0", "@react-pdf-viewer/bookmark": "3.12.0", "@react-pdf-viewer/core": "3.12.0", "@react-pdf-viewer/thumbnail": "3.12.0", "@react-pdf-viewer/toolbar": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-K2fS4+TJynHxxCBFuIDiFuAw3nqOh4bkBgtVZ/2pGvnFn9lLg46YGLMnTXCQqtyZzzXYh696jmlFViun3is4pA=="], + + "@react-pdf-viewer/full-screen": ["@react-pdf-viewer/full-screen@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-hQouJ26QUaRBCXNMU1aI1zpJn4l4PJRvlHhuE2dZYtLl37ycjl7vBCQYZW1FwnuxMWztZsY47R43DKaZORg0pg=="], + + "@react-pdf-viewer/get-file": ["@react-pdf-viewer/get-file@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-Uhq45n2RWlZ7Ec/BtBJ0WQESRciaYIltveDXHNdWvXgFdOS8XsvB+mnTh/wzm7Cfl9hpPyzfeezifdU9AkQgQg=="], + + "@react-pdf-viewer/open": ["@react-pdf-viewer/open@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-vhiDEYsiQLxvZkIKT9VPYHZ1BOnv46x9eCEmRWxO1DJ8fa/GRDTA9ivXmq/ap0dGEJs6t+epleCkCEfllLR/Yw=="], + + "@react-pdf-viewer/page-navigation": ["@react-pdf-viewer/page-navigation@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-tVEJ48Dd5kajV1nKkrPWijglJRNBiKBTyYDKVexhiRdTHUP1f6QQXiSyDgCUb0IGSZeJzOJb1h7ApKHe8OTtuw=="], + + "@react-pdf-viewer/print": ["@react-pdf-viewer/print@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xJn76CgbU/M2iNaN7wLHTg+sdOekkRMfCakFLwPrE+SR7qD6NUF4vQQKJBSVCCK5bUijzb6cWfKGfo8VA72o4Q=="], + + "@react-pdf-viewer/properties": ["@react-pdf-viewer/properties@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-dYTCHtVwFNkpDo7QxL2qk/8zAKndLwdD1FFxBftl6jIlQbtvNdxkFfkv1HcQING9Ic+7DBryOiD7W0ze4IERYg=="], + + "@react-pdf-viewer/rotate": ["@react-pdf-viewer/rotate@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yaxaMYPChvNOjR8+AxRmj0kvojyJKPq4XHEcIB2lJJgBY1Zra3mliDUP3Nlb4yV8BS9+yBqWn9U9mtnopQD+tw=="], + + "@react-pdf-viewer/scroll-mode": ["@react-pdf-viewer/scroll-mode@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-okII7Xqhl6cMvl1izdEvlXNJ+vJVq/qdg53hJIDYVgBCWskLk/cpjUg/ZonBxseG9lIDP3w2VO1McT8Gn11OAg=="], + + "@react-pdf-viewer/search": ["@react-pdf-viewer/search@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-jAkLpis49fsDDY/HrbUZIOIhzF5vynONQNA4INQKI38r/MjveblrkNv7qbr9j5lQ/WFic5+gD1e+Mtpf1/7DiA=="], + + "@react-pdf-viewer/selection-mode": ["@react-pdf-viewer/selection-mode@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yysWEu2aCtBvzSgbhgI9kT5cq2hf0FU6Z+3B7MMXz14Kxyc3y18wUqxtgbvpFEfWF0bNUUq16JtWRljtxvZ83w=="], + + "@react-pdf-viewer/theme": ["@react-pdf-viewer/theme@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cdBi+wR1VOZ6URCcO9plmAZQu4ZGFcd7HJdBe7VIFiGyrvl9I/Of74ONLycnDImSuONt8D3uNjPBLieeaShVeg=="], + + "@react-pdf-viewer/thumbnail": ["@react-pdf-viewer/thumbnail@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-Vc8j3bO6wumWZV4o6pAbktPWKDSC9tQAzOCJ3cof541u4i44C11ccYC4W9aNcsMMUSO3bNwAGWtP8OFthV5akQ=="], + + "@react-pdf-viewer/toolbar": ["@react-pdf-viewer/toolbar@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0", "@react-pdf-viewer/full-screen": "3.12.0", "@react-pdf-viewer/get-file": "3.12.0", "@react-pdf-viewer/open": "3.12.0", "@react-pdf-viewer/page-navigation": "3.12.0", "@react-pdf-viewer/print": "3.12.0", "@react-pdf-viewer/properties": "3.12.0", "@react-pdf-viewer/rotate": "3.12.0", "@react-pdf-viewer/scroll-mode": "3.12.0", "@react-pdf-viewer/search": "3.12.0", "@react-pdf-viewer/selection-mode": "3.12.0", "@react-pdf-viewer/theme": "3.12.0", "@react-pdf-viewer/zoom": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-qACTU3qXHgtNK8J+T13EWio+0liilj86SJ87BdapqXynhl720OKPlSKOQqskUGqg3oTUJAhrse9XG6SFdHJx+g=="], + + "@react-pdf-viewer/zoom": ["@react-pdf-viewer/zoom@3.12.0", "", { "dependencies": { "@react-pdf-viewer/core": "3.12.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-V0GUTyPM77+LzhoKX+T3XI10/HfGdqRTbgeP7ID60FCzcwu6kXWqJn5tzabjDKLTlFv8mJmn0aa/ppkIU97nfA=="], + "@react-stately/flags": ["@react-stately/flags@3.1.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg=="], "@react-stately/utils": ["@react-stately/utils@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw=="], @@ -2217,6 +2280,10 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "make-cancellable-promise": ["make-cancellable-promise@2.0.0", "", {}, "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw=="], + + "make-event-props": ["make-event-props@2.0.0", "", {}, "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw=="], + "map-cache": ["map-cache@0.2.2", "", {}, "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg=="], "mapbox-gl": ["mapbox-gl@3.18.1", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/mapbox-gl-supported": "^3.0.0", "@mapbox/point-geometry": "^1.1.0", "@mapbox/tiny-sdf": "^2.0.6", "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^2.0.4", "@mapbox/whoots-js": "^3.1.0", "@types/geojson": "^7946.0.16", "@types/geojson-vt": "^3.2.5", "@types/mapbox__point-geometry": "^0.1.4", "@types/pbf": "^3.0.5", "@types/supercluster": "^7.1.3", "cheap-ruler": "^4.0.0", "csscolorparser": "~1.0.3", "earcut": "^3.0.1", "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.4", "grid-index": "^1.1.0", "kdbush": "^4.0.2", "martinez-polygon-clipping": "^0.8.1", "murmurhash-js": "^1.0.0", "pbf": "^4.0.1", "potpack": "^2.0.0", "quickselect": "^3.0.0", "supercluster": "^8.0.1", "tinyqueue": "^3.0.0" } }, "sha512-Izc8dee2zkmb6Pn9hXFbVioPRLXJz1OFUcrvri69MhFACPU4bhLyVmhEsD9AyW1qOAP0Yvhzm60v63xdMIHPPw=="], @@ -2231,6 +2298,8 @@ "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + "merge-refs": ["merge-refs@2.0.0", "", { "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "meros": ["meros@1.3.2", "", { "peerDependencies": { "@types/node": ">=13" }, "optionalPeers": ["@types/node"] }, "sha512-Q3mobPbvEx7XbwhnC1J1r60+5H6EZyNccdzSz0eGexJRwouUtTZxPVRGdqKtxlpD84ScK4+tIGldkqDtCKdI0A=="], @@ -2399,6 +2468,8 @@ "pbf": ["pbf@4.0.1", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA=="], + "pdfjs-dist": ["pdfjs-dist@5.4.296", "", { "optionalDependencies": { "@napi-rs/canvas": "^0.1.80" } }, "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q=="], + "permissionless": ["permissionless@0.2.39", "", { "peerDependencies": { "ox": "0.6.7", "viem": "^2.23.2" }, "optionalPeers": ["ox"] }, "sha512-bw6Q41aj0dhwNhPfojzGk+7UTRIGgnzKbnGRlzwB2xI8SI2/yD7EE/IQWm/NbUWHE0yp9GaNOVcqB5mCBFTDNw=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -2555,6 +2626,8 @@ "react-medium-image-zoom": ["react-medium-image-zoom@5.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BsE+EnFVQzFIlyuuQrZ9iTwyKpKkqdFZV1ImEQN573QPqGrIUuNni7aF+sZwDcxlsuOMayCr6oO/PZR/yJnbRg=="], + "react-pdf": ["react-pdf@10.4.0", "", { "dependencies": { "clsx": "^2.0.0", "dequal": "^2.0.3", "make-cancellable-promise": "^2.0.0", "make-event-props": "^2.0.0", "merge-refs": "^2.0.0", "pdfjs-dist": "5.4.296", "tiny-invariant": "^1.0.0", "warning": "^4.0.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-JAuxBX8IXHXuGPVy7d1HbZpAaZF0VIExV3oJByJQM6OWtzn8e2jm6GHRIq7mVY2PKIpYgoLxoglVWm2ITtQ9FQ=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], @@ -2811,6 +2884,8 @@ "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], @@ -2959,6 +3034,8 @@ "wagmi": ["wagmi@3.4.3", "", { "dependencies": { "@wagmi/connectors": "7.1.7", "@wagmi/core": "3.3.3", "use-sync-external-store": "1.4.0" }, "peerDependencies": { "@tanstack/react-query": ">=5.0.0", "react": ">=18", "typescript": ">=5.7.3", "viem": "2.x" }, "optionalPeers": ["typescript"] }, "sha512-g13P1O+1UmCHOi2qgY8Jx0y8xaMPTtgyQN036UQ9ktZtBKfM6GI3O4Ech1ENdlORsedD2Po7WxKw2Ay1FnpTSg=="], + "warning": ["warning@4.0.3", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "web": ["web@workspace:apps/web"],