Anton Pascal ee30f17129 feat: rename properties to projects
- Rename all DB tables: properties → projects, properties_addresses → projects_addresses, properties_models → projects_models, property_likes → projects_likes
- Add projects_likes as proper Drizzle schema table (was SQL-only before)
- Rename ID prefixes: property_xxx → project_xxx
- Rename auth_sessions column: active_property_id → active_project_id
- Simplify project creation: address is now optional (no longer required to get started)
- Update all actions, stores, types, components, and routes
- Rename route: /editor/[propertyId] → /editor/[projectId]
- Add SQL migration for table/column/index/policy/function renames
- Update RPC functions: increment_project_views, get_project_like_count
- Update storage bucket reference: project-thumbnails
- All types/exports follow existing Drizzle patterns (pgTable, relations, zod schemas)
2026-02-17 20:40:52 +00:00

143 lines
3.8 KiB
TypeScript

/**
* Hooks for project model (scene) loading and auto-saving
*/
'use client'
import { useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { useEffect, useRef } from 'react'
import useEditor from '@/store/use-editor'
import { useProjectStore } from '../projects/store'
import { getProjectModel, saveProjectModel } from './actions'
/**
* Load the scene when a project becomes active
* Saves changes automatically with debouncing
*/
export function useProjectScene() {
// Subscribe to project store
const activeProject = useProjectStore((state) => state.activeProject)
const isLoadingProject = useProjectStore((state) => state.isLoading)
const lastProjectIdRef = useRef<string | null>(null)
const saveTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
const isSavingRef = useRef(false)
const currentProjectIdRef = useRef<string | null>(null)
// Extract project ID for dependency tracking
const projectId = activeProject?.id ?? null
const projectName = activeProject?.name ?? null
// Load scene when active project changes
useEffect(() => {
if (isLoadingProject) {
return
}
if (!projectId) {
return
}
// Skip if same project
if (lastProjectIdRef.current === projectId) {
return
}
lastProjectIdRef.current = projectId
// Load the project's scene
async function loadScene() {
try {
const result = await getProjectModel(projectId || '')
if (result.success && result.data?.scene_graph) {
// Load the scene graph into the store
const { nodes, rootNodeIds } = result.data.scene_graph
useScene.getState().setScene(nodes, rootNodeIds)
} else {
// No scene found - clear the scene
useScene.getState().clearScene()
}
} catch (error) {
// Fall back to clear scene
useScene.getState().clearScene()
}
// Reset editor state after loading/clearing scene
useEditor.getState().setPhase('site')
useViewer.getState().setSelection({
buildingId: null,
levelId: null,
selectedIds: [],
zoneId: null,
})
}
loadScene()
}, [projectId, isLoadingProject])
// Auto-save scene changes with debouncing
useEffect(() => {
if (!projectId) {
currentProjectIdRef.current = null
return
}
currentProjectIdRef.current = projectId
// Subscribe to any scene changes
// Use JSON stringification to detect any node changes, not just count
let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
const unsubscribe = useScene.subscribe((state) => {
const currentNodesSnapshot = JSON.stringify(state.nodes)
// Only trigger save if nodes actually changed
if (currentNodesSnapshot === lastNodesSnapshot) {
return
}
lastNodesSnapshot = currentNodesSnapshot
const nodes = state.nodes
// Skip if currently saving
if (isSavingRef.current) {
return
}
// Clear existing timeout
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
// Debounce save by 2 seconds
saveTimeoutRef.current = setTimeout(async () => {
// Get the current project ID at save time (not the captured value)
const currentProjectId = currentProjectIdRef.current
if (!currentProjectId) {
return
}
const rootNodeIds = useScene.getState().rootNodeIds
const sceneGraph = { nodes, rootNodeIds }
isSavingRef.current = true
try {
await saveProjectModel(currentProjectId, sceneGraph)
} finally {
isSavingRef.current = false
}
}, 2000)
})
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
unsubscribe()
}
}, [projectId])
}