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
8 changes: 8 additions & 0 deletions apps/web/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export const VALID_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'imag
export const IMAGE_ACCEPT = VALID_IMAGE_TYPES.join(',');
export const MAX_IMAGE_SIZE_BYTES = 20 * 1024 * 1024; // 20MB

// PDF file types and upload constraints
export const VALID_PDF_TYPES = ['application/pdf'];
export const PDF_ACCEPT = VALID_PDF_TYPES.join(',');
export const MAX_PDF_SIZE_BYTES = 50 * 1024 * 1024; // 50MB

export const DATA_TYPE_PROPERTY = '6d29d57849bb4959baf72cc696b1671a';

/** DataType → entity ID for Data Type relations. */
Expand Down Expand Up @@ -78,6 +83,9 @@ export const DOCUMENTATION_SPACE_ENTITY_ID = '46162f0614d448b2b00d8c3bbd7e5194';
export const GEO_LOCATION = '9cf5c1b015dc451cbfd297db64806aff';
export const FORMAT_PROPERTY = '396f8c72dfd04b5791ea09c1b9321b2f';

export const PDF_TYPE = '14a39e59d9874596956ac2dd4165c210';
export const PDF_URL = '9d69ac47702343e2b72a91a335eb54d9';

export const PLACE = '783bc688e65f4e54b67fa5643d78345e';
export const ADDRESS = '5c6e72fb834047c082818be159ecd495';
export const VENUE_PROPERTY = 'f28bbb6bf4e8465d9de7a09085e224b9';
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/lite';

import { ADDRESS, GEO_LOCATION, PLACE, VIDEO_RENDERABLE_TYPE } from '~/core/constants';
import { ADDRESS, GEO_LOCATION, PDF_TYPE, PLACE, VIDEO_RENDERABLE_TYPE } from '~/core/constants';
import { DataType as AppDataType, Property, RenderableType } from '~/core/types';
import { getDataTypeFromEntityId } from '~/core/utils/property/properties';

Expand Down Expand Up @@ -34,6 +34,8 @@ export function getStrictRenderableType(renderableType: string | null): Renderab
return 'IMAGE';
case VIDEO_RENDERABLE_TYPE:
return 'VIDEO';
case PDF_TYPE:
return 'PDF';
case SystemIds.URL:
return 'URL';
case GEO_LOCATION:
Expand Down
12 changes: 10 additions & 2 deletions apps/web/core/io/dto/relations.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { SystemIds } from '@geoprotocol/geo-sdk/lite';

import { PDF_TYPE, PDF_URL } from '~/core/constants';
import { RemoteEntityType, RemoteRelation } from '~/core/io/schema';
import { Relation, RenderableEntityType } from '~/core/types';
import { getSpaceRank } from '~/core/utils/space/space-ranking';

export function RelationDtoLive(relation: RemoteRelation): Relation {
const ipfsUrlPropertyHex = SystemIds.IMAGE_URL_PROPERTY.replace(/-/g, '');
const mediaEntityUrlValue = relation.toEntity.valuesList.find(v => v.propertyId === ipfsUrlPropertyHex)?.text ?? null;
const pdfUrlPropertyHex = PDF_URL; // Already hex format
const imageUrlValue = relation.toEntity.valuesList.find(v => v.propertyId === ipfsUrlPropertyHex)?.text ?? null;
const pdfUrlValue = relation.toEntity.valuesList.find(v => v.propertyId === pdfUrlPropertyHex)?.text ?? null;
const mediaEntityUrlValue = imageUrlValue ?? pdfUrlValue;
const baseRenderableType = v2_getRenderableEntityType(relation.toEntity.types);

const renderableType = mediaEntityUrlValue && baseRenderableType === 'RELATION' ? 'IMAGE' : baseRenderableType;
Expand All @@ -33,7 +37,7 @@ export function RelationDtoLive(relation: RemoteRelation): Relation {
toEntity: {
id: toEntityId,
name: toEntityName,
value: renderableType === 'IMAGE' || renderableType === 'VIDEO' ? (mediaEntityUrlValue ?? '') : toEntityId,
value: renderableType === 'IMAGE' || renderableType === 'VIDEO' || renderableType === 'PDF' ? (mediaEntityUrlValue ?? '') : toEntityId,
},
};
}
Expand Down Expand Up @@ -70,6 +74,10 @@ function v2_getRenderableEntityType(types: readonly RemoteEntityType[]): Rendera
return 'VIDEO';
}

if (typeIds.includes(PDF_TYPE)) {
return 'PDF';
}

if (typeIds.includes(dataBlockHex)) {
return 'DATA';
}
Expand Down
15 changes: 13 additions & 2 deletions apps/web/core/io/dto/snapshot-to-diff.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SystemIds } from '@geoprotocol/geo-sdk/lite';

import { PDF_TYPE, PDF_URL } from '~/core/constants';
import type { BlockChange, EntityDiff, RelationChange, SimpleValueType, ValueChange } from '~/core/utils/diff/types';

import type { ApiBlockSnapshot, ApiEntitySnapshotResponse, ApiVersionedRelation, ApiVersionedValue } from '../rest';
Expand All @@ -22,17 +23,18 @@ const {
* Determine block type from a TYPES_PROPERTY relation.
* Shared logic with `detectBlockType` in diff.ts, but operates on raw API relation shapes.
*/
function classifyBlockType(typeEntityId: string): 'textBlock' | 'imageBlock' | 'videoBlock' | 'dataBlock' | null {
function classifyBlockType(typeEntityId: string): 'textBlock' | 'imageBlock' | 'videoBlock' | 'pdfBlock' | 'dataBlock' | null {
if (typeEntityId === TEXT_BLOCK) return 'textBlock';
if (typeEntityId === IMAGE_BLOCK || typeEntityId === IMAGE_TYPE) return 'imageBlock';
if (typeEntityId === VIDEO_TYPE || typeEntityId === VIDEO_BLOCK) return 'videoBlock';
if (typeEntityId === PDF_TYPE) return 'pdfBlock';
if (typeEntityId === DATA_BLOCK) return 'dataBlock';
return null;
}

function getBlockTypeFromRelations(
relations: readonly ApiVersionedRelation[]
): 'textBlock' | 'imageBlock' | 'videoBlock' | 'dataBlock' {
): 'textBlock' | 'imageBlock' | 'videoBlock' | 'pdfBlock' | 'dataBlock' {
for (const rel of relations) {
if (rel.typeId === TYPES_PROPERTY) {
const blockType = classifyBlockType(rel.toEntityId);
Expand Down Expand Up @@ -137,6 +139,15 @@ function snapshotBlockToChange(block: ApiBlockSnapshot): BlockChange {
after: url,
};
}
case 'pdfBlock': {
const url = block.values.find(v => v.propertyId === PDF_URL)?.text ?? null;
return {
id: block.id,
type: 'pdfBlock',
before: null,
after: url,
};
}
case 'dataBlock': {
const name = block.values.find(v => v.propertyId === NAME_PROPERTY)?.text ?? null;
return {
Expand Down
1 change: 1 addition & 0 deletions apps/web/core/io/rest/schemas/diff-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const ApiBlockDiffSchema = Schema.Struct({
Schema.Literal('textBlock'),
Schema.Literal('imageBlock'),
Schema.Literal('videoBlock'),
Schema.Literal('pdfBlock'),
Schema.Literal('dataBlock')
),
before: Schema.NullOr(Schema.String),
Expand Down
5 changes: 4 additions & 1 deletion apps/web/core/state/editor/block-types.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { IdUtils, Position, SystemIds } from '@geoprotocol/geo-sdk/lite';

import { PDF_TYPE } from '~/core/constants';
import { EntityId } from '~/core/io/substream-schema';
import { Relation } from '~/core/types';

type BlockTypeId =
| typeof SystemIds.TEXT_BLOCK
| typeof SystemIds.IMAGE_TYPE
| typeof SystemIds.DATA_BLOCK
| typeof SystemIds.VIDEO_TYPE;
| typeof SystemIds.VIDEO_TYPE
| typeof PDF_TYPE;

const BLOCK_TYPE_NAMES: Record<BlockTypeId, string> = {
[SystemIds.TEXT_BLOCK]: 'Text Block',
[SystemIds.IMAGE_TYPE]: 'Image',
[SystemIds.DATA_BLOCK]: 'Data Block',
[SystemIds.VIDEO_TYPE]: 'Video',
[PDF_TYPE]: 'PDF',
};

export function getRelationForBlockType(
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 @@ -45,6 +45,14 @@ export type Content =
id: string;
src: string;
};
}
| {
type: 'pdf';
content: Content[];
attrs: {
id: string;
src: string;
};
};

type Mark = {
Expand Down
38 changes: 38 additions & 0 deletions apps/web/core/state/editor/use-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useSearchParams } from 'next/navigation';
import { storage } from '~/core/sync/use-mutate';
import { getRelations, getValues, useValues } from '~/core/sync/use-store';
import { Relation, RenderableEntityType, Value } from '~/core/types';
import { PDF_TYPE, PDF_URL } from '~/core/constants';
import { getImagePath, getVideoPath, validateEntityId } from '~/core/utils/utils';

import type { ServerBlock } from '~/partials/editor/server-content';
Expand Down Expand Up @@ -100,6 +101,8 @@ function makeNewBlockRelation({
return 'IMAGE';
case 'video':
return 'VIDEO';
case 'pdf':
return 'PDF';
default:
return 'TEXT';
}
Expand Down Expand Up @@ -349,6 +352,30 @@ export function useEditorStore() {
];
}

if (toEntity?.type === 'PDF') {
// Read PDF URL from Values using PDF_URL property
const pdfUrlValues = getValues({
mergeWith: initialBlockValues,
selector: value => value.entity.id === block.block.id && value.property.id === PDF_URL,
});
const pdfUrlValue = pdfUrlValues?.[0]?.value || toEntity.value || '';
const pdfSrc = pdfUrlValue ? getImagePath(pdfUrlValue) : '';

sBlocks.push({ type: 'pdf', src: pdfSrc });

return [
{
type: 'pdf',
attrs: {
id: block.block.id,
src: pdfSrc,
relationId: block.relationId,
spaceId,
},
},
];
}

if (toEntity?.type === 'DATA') {
sBlocks.push({ type: 'data' });

Expand Down Expand Up @@ -472,6 +499,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 All @@ -498,6 +527,12 @@ export function useEditorStore() {
storage.relations.set(relation);
break;
}
case PDF_TYPE: {
// Create a Types relation to mark this entity as a PDF type
const relation = getRelationForBlockType(node.id, PDF_TYPE, spaceId);
storage.relations.set(relation);
break;
}
case SystemIds.DATA_BLOCK: {
// @TODO(performance): upsertMany
for (const relation of makeInitialDataEntityRelations(EntityId(node.id), spaceId)) {
Expand Down Expand Up @@ -550,6 +585,9 @@ export function useEditorStore() {
// Video block persistence is handled directly in video-node.tsx component
// using storage.entities.name.set() for title
break;
case 'pdf':
// PDF block persistence is handled directly in pdf-node.tsx component
break;
default:
break;
}
Expand Down
Loading
Loading