Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 3 additions & 1 deletion apps/web/core/io/dto/properties.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions apps/web/core/state/editor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export type Content =
id: string;
src: string;
};
}
| {
type: 'pdf';
content: Content[];
attrs: {
id: string;
src: string;
};
};

type Mark = {
Expand Down
19 changes: 19 additions & 0 deletions apps/web/core/state/editor/use-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -99,6 +100,8 @@ function makeNewBlockRelation({
return 'IMAGE';
case 'video':
return 'VIDEO';
case 'pdf':
return 'PDF';
}
})();

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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;
}
Expand Down
115 changes: 106 additions & 9 deletions apps/web/core/sync/use-mutate.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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://');
});
Expand Down Expand Up @@ -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);
},
Expand Down
7 changes: 4 additions & 3 deletions apps/web/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -135,7 +135,7 @@ export type SwitchableRenderableType =
| 'POINT'
| 'GEO_LOCATION'
| 'PLACE'
| 'ADDRESS';
| 'ADDRESS' | 'PDF';

/**
* Human-readable labels for switchable renderable types
Expand All @@ -157,6 +157,7 @@ export const SWITCHABLE_RENDERABLE_TYPE_LABELS: Record<SwitchableRenderableType,
GEO_LOCATION: 'Geo Location',
PLACE: 'Place',
ADDRESS: 'Address',
PDF: 'PDF'
};

// ==============================================================================
Expand Down
1 change: 1 addition & 0 deletions apps/web/core/utils/property/properties.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ vi.mock('~/core/constants', () => ({
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',
}));

Expand Down
7 changes: 7 additions & 0 deletions apps/web/core/utils/property/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DATA_TYPE_PROPERTY,
FORMAT_PROPERTY,
GEO_LOCATION,
PDF_TYPE,
PLACE,
RENDERABLE_TYPE_PROPERTY,
UNIT_PROPERTY,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -169,6 +175,7 @@ export const typeToBaseDataType: Record<SwitchableRenderableType, DataType> = {
POINT: 'POINT',
GEO_LOCATION: 'POINT',
PLACE: 'RELATION',
PDF: 'RELATION',
ADDRESS: 'RELATION',
} as const;

Expand Down
4 changes: 2 additions & 2 deletions apps/web/core/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions apps/web/design-system/chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -318,7 +323,7 @@ function RelationDots({ color }: RelationDotsProps) {

type LinkableMediaChipProps = {
isEditing: boolean;
mediaType: 'IMAGE' | 'VIDEO';
mediaType: 'IMAGE' | 'VIDEO' | 'PDF';
mediaSrc?: string;
isUploading?: boolean;

Expand Down Expand Up @@ -402,7 +407,7 @@ export function LinkableMediaChip({
href={NavUtils.toEntity(spaceId ?? currentSpaceId, entityId)}
className="block"
>
<div className="relative h-20 w-20">
<div className={`relative ${mediaType === 'PDF' ? 'h-[200px] w-[173px]' : 'h-20 w-20'}`}>
{mediaType === 'VIDEO' ? (
videoSrc ? (
<video
Expand All @@ -418,8 +423,10 @@ export function LinkableMediaChip({
<VideoSmallIcon color="grey-04" />
</div>
)
) : mediaSrc ? (
) : mediaSrc && mediaType === 'IMAGE' ? (
<GeoImage fill value={mediaSrc} alt="" className="object-cover" />
) : mediaSrc && mediaType === 'PDF' ? (
<PdfZoom pdfSrc={mediaSrc} isEditing />
) : (
<div className="flex h-full w-full items-center justify-center bg-grey-01">
<ImageIcon color="grey-04" />
Expand All @@ -429,7 +436,7 @@ export function LinkableMediaChip({
</Link>

{/* Upload button overlay - only in edit mode */}
{isEditing && onUpload && !isUploading && (
{isEditing && mediaType !== 'PDF' && onUpload && !isUploading && (
<button
onClick={e => {
e.stopPropagation();
Expand Down
Loading
Loading