@buley/hexgrid-3d 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +28 -0
- package/LICENSE +39 -0
- package/README.md +291 -0
- package/examples/basic-usage.tsx +52 -0
- package/package.json +65 -0
- package/public/hexgrid-worker.js +1763 -0
- package/rust/Cargo.toml +41 -0
- package/rust/src/lib.rs +740 -0
- package/rust/src/math.rs +574 -0
- package/rust/src/spatial.rs +245 -0
- package/rust/src/statistics.rs +496 -0
- package/src/HexGridEnhanced.ts +16 -0
- package/src/Snapshot.ts +1402 -0
- package/src/adapters.ts +65 -0
- package/src/algorithms/AdvancedStatistics.ts +328 -0
- package/src/algorithms/BayesianStatistics.ts +317 -0
- package/src/algorithms/FlowField.ts +126 -0
- package/src/algorithms/FluidSimulation.ts +99 -0
- package/src/algorithms/GraphAlgorithms.ts +184 -0
- package/src/algorithms/OutlierDetection.ts +391 -0
- package/src/algorithms/ParticleSystem.ts +85 -0
- package/src/algorithms/index.ts +13 -0
- package/src/compat.ts +96 -0
- package/src/components/HexGrid.tsx +31 -0
- package/src/components/NarrationOverlay.tsx +221 -0
- package/src/components/index.ts +2 -0
- package/src/features.ts +125 -0
- package/src/index.ts +30 -0
- package/src/math/HexCoordinates.ts +15 -0
- package/src/math/Matrix4.ts +35 -0
- package/src/math/Quaternion.ts +37 -0
- package/src/math/SpatialIndex.ts +114 -0
- package/src/math/Vector3.ts +69 -0
- package/src/math/index.ts +11 -0
- package/src/note-adapter.ts +124 -0
- package/src/ontology-adapter.ts +77 -0
- package/src/stores/index.ts +1 -0
- package/src/stores/uiStore.ts +85 -0
- package/src/types/index.ts +3 -0
- package/src/types.ts +152 -0
- package/src/utils/image-utils.ts +25 -0
- package/src/wasm/HexGridWasmWrapper.ts +753 -0
- package/src/wasm/index.ts +7 -0
- package/src/workers/hexgrid-math.ts +177 -0
- package/src/workers/hexgrid-worker.worker.ts +1807 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter for embedding notes directly in the grid
|
|
3
|
+
*
|
|
4
|
+
* This adapter allows notes (Cyrano's internal notes, user notes, etc.)
|
|
5
|
+
* to be embedded directly in GridItems with full metadata preservation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { GridItem } from './types'
|
|
9
|
+
import type { ItemAdapter, AdapterOptions } from './adapters'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Note content structure
|
|
13
|
+
*/
|
|
14
|
+
export interface NoteContent {
|
|
15
|
+
title?: string
|
|
16
|
+
text: string
|
|
17
|
+
summary?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Note metadata structure
|
|
22
|
+
*/
|
|
23
|
+
export interface NoteMetadata {
|
|
24
|
+
type?: string
|
|
25
|
+
category?: string
|
|
26
|
+
subcategory?: string
|
|
27
|
+
tags?: string[]
|
|
28
|
+
targetUserId?: string
|
|
29
|
+
status?: 'active' | 'archived' | 'draft'
|
|
30
|
+
priority?: 'low' | 'medium' | 'high' | 'urgent'
|
|
31
|
+
context?: string
|
|
32
|
+
permissions?: 'private' | 'shared' | 'public'
|
|
33
|
+
sharedWith?: string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Note structure matching Affectively's note format
|
|
38
|
+
*/
|
|
39
|
+
export interface Note {
|
|
40
|
+
id: string
|
|
41
|
+
content: NoteContent
|
|
42
|
+
metadata?: NoteMetadata
|
|
43
|
+
date: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Calculate velocity for a note based on recency, priority, and metadata richness
|
|
48
|
+
*/
|
|
49
|
+
function calculateNoteVelocity(note: Note): number {
|
|
50
|
+
let velocity = 0.1 // Base minimum
|
|
51
|
+
|
|
52
|
+
// Priority contribution (0-0.3)
|
|
53
|
+
const priorityMap: Record<string, number> = {
|
|
54
|
+
urgent: 0.3,
|
|
55
|
+
high: 0.2,
|
|
56
|
+
medium: 0.1,
|
|
57
|
+
low: 0.05,
|
|
58
|
+
}
|
|
59
|
+
if (note.metadata?.priority) {
|
|
60
|
+
velocity += priorityMap[note.metadata.priority] || 0.1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Recency contribution (0-0.4)
|
|
64
|
+
if (note.date) {
|
|
65
|
+
const ageMs = Date.now() - new Date(note.date).getTime()
|
|
66
|
+
const ageHours = ageMs / (1000 * 60 * 60)
|
|
67
|
+
const recencyFactor = Math.max(0, 1 - ageHours / 168) // Decay over 1 week
|
|
68
|
+
velocity += recencyFactor * 0.4
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Metadata richness contribution (0-0.2)
|
|
72
|
+
let contextScore = 0
|
|
73
|
+
if (note.metadata?.type) contextScore += 0.05
|
|
74
|
+
if (note.metadata?.category) contextScore += 0.05
|
|
75
|
+
if (note.metadata?.tags && note.metadata.tags.length > 0) contextScore += 0.05
|
|
76
|
+
if (note.metadata?.context) contextScore += 0.05
|
|
77
|
+
velocity += Math.min(contextScore, 0.2)
|
|
78
|
+
|
|
79
|
+
// Clamp to [0.1, 1.0]
|
|
80
|
+
return Math.max(0.1, Math.min(1.0, velocity))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Adapter for embedding notes directly
|
|
85
|
+
*/
|
|
86
|
+
export const noteAdapter: ItemAdapter<Note> = {
|
|
87
|
+
toGridItem(note: Note, options?: AdapterOptions): GridItem<Note> {
|
|
88
|
+
const velocity = options?.velocity ?? calculateNoteVelocity(note)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: note.id,
|
|
92
|
+
type: 'note',
|
|
93
|
+
title: note.content.title || 'Untitled Note',
|
|
94
|
+
description: note.content.summary || note.content.text.substring(0, 200),
|
|
95
|
+
data: note,
|
|
96
|
+
createdAt: note.date,
|
|
97
|
+
velocity,
|
|
98
|
+
// Notes can have generated visualizations
|
|
99
|
+
imageUrl: options?.visualUrl || `/api/notes/${note.id}/visualization`,
|
|
100
|
+
category: note.metadata?.category,
|
|
101
|
+
// Store metadata in metrics for filtering/sorting
|
|
102
|
+
metrics: {
|
|
103
|
+
priority: note.metadata?.priority === 'urgent' ? 4 : note.metadata?.priority === 'high' ? 3 : note.metadata?.priority === 'medium' ? 2 : 1,
|
|
104
|
+
tagCount: note.metadata?.tags?.length || 0,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
fromGridItem(item: GridItem<Note>): Note {
|
|
110
|
+
if (!item.data) {
|
|
111
|
+
throw new Error('GridItem missing note data')
|
|
112
|
+
}
|
|
113
|
+
return item.data
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
calculateVelocity(note: Note): number {
|
|
117
|
+
return calculateNoteVelocity(note)
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
extractVisualUrl(note: Note): string | undefined {
|
|
121
|
+
// Notes can have generated visualizations
|
|
122
|
+
return `/api/notes/${note.id}/visualization`
|
|
123
|
+
},
|
|
124
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter for embedding ontology entities directly in the grid
|
|
3
|
+
*
|
|
4
|
+
* This adapter allows ontology entities from @emotions-app/shared-utils/ontology
|
|
5
|
+
* to be embedded directly in GridItems with full metadata preservation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OntologyEntity } from '@emotions-app/shared-utils/ontology/types'
|
|
9
|
+
import type { GridItem } from './types'
|
|
10
|
+
import type { ItemAdapter, AdapterOptions } from './adapters'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Calculate velocity for an ontology entity based on provenance confidence
|
|
14
|
+
* and recency
|
|
15
|
+
*/
|
|
16
|
+
function calculateEntityVelocity(entity: OntologyEntity): number {
|
|
17
|
+
let velocity = 0.1 // Base minimum
|
|
18
|
+
|
|
19
|
+
// Use provenance confidence (0-1) as primary factor
|
|
20
|
+
const confidence = entity.metadata.provenance.confidence
|
|
21
|
+
velocity += confidence * 0.5
|
|
22
|
+
|
|
23
|
+
// Recency factor based on lastModified
|
|
24
|
+
if (entity.metadata.lastModified) {
|
|
25
|
+
const ageMs = Date.now() - new Date(entity.metadata.lastModified).getTime()
|
|
26
|
+
const ageHours = ageMs / (1000 * 60 * 60)
|
|
27
|
+
const recencyFactor = Math.max(0, 1 - ageHours / 168) // Decay over 1 week
|
|
28
|
+
velocity += recencyFactor * 0.4
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Clamp to [0.1, 1.0]
|
|
32
|
+
return Math.max(0.1, Math.min(1.0, velocity))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Adapter for embedding ontology entities directly
|
|
37
|
+
*/
|
|
38
|
+
export const ontologyEntityAdapter: ItemAdapter<OntologyEntity> = {
|
|
39
|
+
toGridItem(entity: OntologyEntity, options?: AdapterOptions): GridItem<OntologyEntity> {
|
|
40
|
+
const velocity = options?.velocity ?? calculateEntityVelocity(entity)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: entity['@id'],
|
|
44
|
+
type: 'ontology-entity',
|
|
45
|
+
title: entity.label,
|
|
46
|
+
description: (entity.properties.description as string | undefined) || entity.label,
|
|
47
|
+
data: entity,
|
|
48
|
+
ontologyMetadata: {
|
|
49
|
+
entityId: entity['@id'],
|
|
50
|
+
entityType: Array.isArray(entity['@type'])
|
|
51
|
+
? entity['@type']
|
|
52
|
+
: [entity['@type']],
|
|
53
|
+
properties: entity.properties,
|
|
54
|
+
provenance: entity.metadata.provenance,
|
|
55
|
+
},
|
|
56
|
+
velocity,
|
|
57
|
+
createdAt: entity.metadata.lastModified || entity.metadata.provenance.extractedAt,
|
|
58
|
+
// Extract visual URL if available in properties
|
|
59
|
+
imageUrl: options?.visualUrl || (entity.properties.imageUrl as string | undefined),
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
fromGridItem(item: GridItem<OntologyEntity>): OntologyEntity {
|
|
64
|
+
if (!item.data) {
|
|
65
|
+
throw new Error('GridItem missing ontology entity data')
|
|
66
|
+
}
|
|
67
|
+
return item.data
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
calculateVelocity(entity: OntologyEntity): number {
|
|
71
|
+
return calculateEntityVelocity(entity)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
extractVisualUrl(entity: OntologyEntity): string | undefined {
|
|
75
|
+
return entity.properties.imageUrl as string | undefined
|
|
76
|
+
},
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const hexgridStores = {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
type UIState = {
|
|
2
|
+
debugOpen: boolean
|
|
3
|
+
showStats: boolean
|
|
4
|
+
cameraOpen?: boolean
|
|
5
|
+
showNarration?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Safe localStorage helpers that never throw
|
|
9
|
+
const safeGetItem = (key: string): string | null => {
|
|
10
|
+
// istanbul ignore next
|
|
11
|
+
if (typeof window === 'undefined') return null
|
|
12
|
+
try {
|
|
13
|
+
return window.localStorage.getItem(key)
|
|
14
|
+
} catch {
|
|
15
|
+
// istanbul ignore next
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const safeSetItem = (key: string, value: string): void => {
|
|
21
|
+
// istanbul ignore next
|
|
22
|
+
if (typeof window === 'undefined') return
|
|
23
|
+
try {
|
|
24
|
+
window.localStorage.setItem(key, value)
|
|
25
|
+
} catch {
|
|
26
|
+
// istanbul ignore next - private browsing, quota exceeded
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Initialize showNarration from localStorage if available
|
|
31
|
+
const savedNarration = safeGetItem('hexgrid.showNarration')
|
|
32
|
+
const initialShowNarration = savedNarration === 'true'
|
|
33
|
+
|
|
34
|
+
const state: UIState = {
|
|
35
|
+
debugOpen: false,
|
|
36
|
+
showStats: false,
|
|
37
|
+
cameraOpen: false,
|
|
38
|
+
showNarration: initialShowNarration
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const listeners = new Set<(s: UIState) => void>()
|
|
42
|
+
|
|
43
|
+
const uiStore = {
|
|
44
|
+
getState(): UIState {
|
|
45
|
+
return { ...state }
|
|
46
|
+
},
|
|
47
|
+
set(partial: Partial<UIState>) {
|
|
48
|
+
let changed = false
|
|
49
|
+
for (const k of Object.keys(partial) as (keyof UIState)[]) {
|
|
50
|
+
if (partial[k] !== undefined && state[k] !== partial[k]) {
|
|
51
|
+
// @ts-ignore
|
|
52
|
+
state[k] = partial[k]
|
|
53
|
+
changed = true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (changed) {
|
|
57
|
+
// Persist showNarration to localStorage for cross-refresh consistency
|
|
58
|
+
if (partial.showNarration !== undefined) {
|
|
59
|
+
safeSetItem('hexgrid.showNarration', String(!!partial.showNarration))
|
|
60
|
+
}
|
|
61
|
+
for (const cb of Array.from(listeners)) cb({ ...state })
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
subscribe(cb: (s: UIState) => void) {
|
|
65
|
+
listeners.add(cb)
|
|
66
|
+
// emit current immediately
|
|
67
|
+
cb({ ...state })
|
|
68
|
+
return () => listeners.delete(cb)
|
|
69
|
+
},
|
|
70
|
+
toggleDebug() {
|
|
71
|
+
this.set({ debugOpen: !state.debugOpen })
|
|
72
|
+
},
|
|
73
|
+
toggleStats() {
|
|
74
|
+
this.set({ showStats: !state.showStats })
|
|
75
|
+
},
|
|
76
|
+
toggleCamera() {
|
|
77
|
+
this.set({ cameraOpen: !state.cameraOpen })
|
|
78
|
+
},
|
|
79
|
+
toggleNarration() {
|
|
80
|
+
this.set({ showNarration: !state.showNarration })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { uiStore }
|
|
85
|
+
export default uiStore
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for HexGrid Visualization
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RefObject } from 'react'
|
|
6
|
+
import type { HexGridFeatureFlags } from './features'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Photo type for HexGrid visualization
|
|
10
|
+
*
|
|
11
|
+
* This is the unified Photo type used throughout the hexgrid-3d package.
|
|
12
|
+
* It includes all fields needed for display, media playback, and analytics.
|
|
13
|
+
*/
|
|
14
|
+
export interface Photo {
|
|
15
|
+
id: string
|
|
16
|
+
title: string
|
|
17
|
+
|
|
18
|
+
// Image URLs - imageUrl is primary, url is for backward compatibility
|
|
19
|
+
imageUrl: string
|
|
20
|
+
url?: string // Alias for imageUrl (backward compatibility)
|
|
21
|
+
thumbnailUrl?: string
|
|
22
|
+
|
|
23
|
+
// Display metadata
|
|
24
|
+
alt: string
|
|
25
|
+
category: string
|
|
26
|
+
description?: string
|
|
27
|
+
|
|
28
|
+
// Source information
|
|
29
|
+
source: string
|
|
30
|
+
sourceUrl?: string
|
|
31
|
+
createdAt?: string
|
|
32
|
+
|
|
33
|
+
// Shop integration
|
|
34
|
+
shopUrl?: string
|
|
35
|
+
location?: string
|
|
36
|
+
|
|
37
|
+
// Media type flags
|
|
38
|
+
isVideo?: boolean
|
|
39
|
+
videoUrl?: string
|
|
40
|
+
isTweet?: boolean
|
|
41
|
+
tweetUrl?: string
|
|
42
|
+
redditUrl?: string
|
|
43
|
+
durationSeconds?: number
|
|
44
|
+
|
|
45
|
+
// Competition/ranking system
|
|
46
|
+
velocity?: number // Normalized velocity [0.1, 1.0] for meritocratic competition
|
|
47
|
+
|
|
48
|
+
// User info
|
|
49
|
+
userId?: string
|
|
50
|
+
username?: string
|
|
51
|
+
platform?: string
|
|
52
|
+
author?: string
|
|
53
|
+
authorUrl?: string
|
|
54
|
+
|
|
55
|
+
// Metrics for analytics
|
|
56
|
+
views?: number
|
|
57
|
+
likes?: number
|
|
58
|
+
comments?: number
|
|
59
|
+
shares?: number
|
|
60
|
+
upvotes?: number
|
|
61
|
+
retweets?: number
|
|
62
|
+
replies?: number
|
|
63
|
+
age_in_hours?: number
|
|
64
|
+
|
|
65
|
+
// Visual
|
|
66
|
+
dominantColor?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generic item that can represent any data object in the grid
|
|
71
|
+
* Extends Photo for backward compatibility while adding generic data support
|
|
72
|
+
*/
|
|
73
|
+
export interface GridItem<T = unknown> {
|
|
74
|
+
// Core identification
|
|
75
|
+
id: string
|
|
76
|
+
type: string // 'photo' | 'note' | 'emotion' | 'ontology-entity' | 'custom'
|
|
77
|
+
|
|
78
|
+
// Visual representation (optional - allows non-visual items)
|
|
79
|
+
imageUrl?: string
|
|
80
|
+
thumbnailUrl?: string
|
|
81
|
+
videoUrl?: string
|
|
82
|
+
|
|
83
|
+
// Display metadata
|
|
84
|
+
title?: string
|
|
85
|
+
alt?: string
|
|
86
|
+
description?: string
|
|
87
|
+
category?: string
|
|
88
|
+
|
|
89
|
+
// Embedded data object (preserves original type)
|
|
90
|
+
data?: T
|
|
91
|
+
|
|
92
|
+
// Ontology metadata (optional, for ontology-aware items)
|
|
93
|
+
ontologyMetadata?: {
|
|
94
|
+
entityId?: string
|
|
95
|
+
entityType?: string | string[]
|
|
96
|
+
properties?: Record<string, unknown>
|
|
97
|
+
provenance?: {
|
|
98
|
+
source: string
|
|
99
|
+
extractedAt: string
|
|
100
|
+
confidence: number
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Grid behavior
|
|
105
|
+
velocity?: number
|
|
106
|
+
source?: string
|
|
107
|
+
sourceUrl?: string
|
|
108
|
+
createdAt?: string
|
|
109
|
+
|
|
110
|
+
// Metrics (flexible for any data type)
|
|
111
|
+
metrics?: Record<string, number>
|
|
112
|
+
|
|
113
|
+
// Legacy Photo fields (for backward compatibility)
|
|
114
|
+
url?: string // Maps to imageUrl
|
|
115
|
+
userId?: string
|
|
116
|
+
username?: string
|
|
117
|
+
platform?: string
|
|
118
|
+
author?: string
|
|
119
|
+
authorUrl?: string
|
|
120
|
+
likes?: number
|
|
121
|
+
views?: number
|
|
122
|
+
comments?: number
|
|
123
|
+
dominantColor?: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface HexGridProps {
|
|
127
|
+
photos: Photo[]
|
|
128
|
+
onHexClick?: (photo: Photo) => void
|
|
129
|
+
spacing?: number
|
|
130
|
+
canvasRef?: RefObject<HTMLCanvasElement>
|
|
131
|
+
onLeaderboardUpdate?: (leaderboard: any) => void
|
|
132
|
+
autoplayQueueLimit?: number
|
|
133
|
+
onAutoplayQueueLimitChange?: (limit: number) => void
|
|
134
|
+
modalOpen?: boolean
|
|
135
|
+
userId?: string
|
|
136
|
+
username?: string
|
|
137
|
+
featureFlags?: HexGridFeatureFlags
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface UIState {
|
|
141
|
+
debugOpen: boolean
|
|
142
|
+
showStats: boolean
|
|
143
|
+
cameraOpen: boolean
|
|
144
|
+
showNarration: boolean
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface WorkerDebug {
|
|
148
|
+
curveUDeg: number
|
|
149
|
+
curveVDeg: number
|
|
150
|
+
batchPerFrame: number
|
|
151
|
+
[key: string]: any
|
|
152
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions for HexGrid component.
|
|
3
|
+
* These functions have NO side effects and are deterministic.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get proxied image URL for preview.redd.it images (CORS issues).
|
|
8
|
+
* External preview URLs work fine without proxying.
|
|
9
|
+
* @pure - No side effects, deterministic
|
|
10
|
+
* @param imageUrl Original image URL
|
|
11
|
+
* @returns Proxied URL if needed, otherwise original URL
|
|
12
|
+
*/
|
|
13
|
+
export function getProxiedImageUrl(imageUrl: string): string {
|
|
14
|
+
if (!imageUrl || typeof imageUrl !== 'string') {
|
|
15
|
+
return imageUrl
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Only proxy preview.redd.it URLs (they have CORS issues)
|
|
19
|
+
// external-preview.redd.it URLs work fine, so don't proxy them
|
|
20
|
+
if (imageUrl.includes('preview.redd.it') && !imageUrl.includes('external-preview.redd.it')) {
|
|
21
|
+
return `/api/proxy-image?url=${encodeURIComponent(imageUrl)}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return imageUrl
|
|
25
|
+
}
|