@@ -115,7 +117,7 @@ export function PropertySettingsDialog({
Privacy
- {isPrivate ? 'Only you can view this property' : 'Anyone can view this property'}
+ {isPrivate ? 'Only you can view this project' : 'Anyone can view this project'}
@@ -205,7 +207,7 @@ export function PropertySettingsDialog({
Danger Zone
- Once you delete a property, there is no going back. Please be certain.
+ Once you delete a project, there is no going back. Please be certain.
diff --git a/apps/editor/features/community/components/property-dropdown.tsx b/apps/editor/features/community/components/property-dropdown.tsx
deleted file mode 100644
index 095cdee8..00000000
--- a/apps/editor/features/community/components/property-dropdown.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-'use client'
-
-import { Check, ChevronDown, Home, Plus } from 'lucide-react'
-import { useState } from 'react'
-import { usePropertyStore } from '../lib/properties/store'
-import { cn } from '@/lib/utils'
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from '@/components/ui/primitives/dropdown-menu'
-import { NewPropertyDialog } from './new-property-dialog'
-import { usePropertyScene } from '../lib/models/hooks'
-
-/**
- * PropertyDropdown - Shows active property and allows switching between properties
- */
-export function PropertyDropdown() {
- usePropertyScene() // Load and auto-save property scenes
-
- // Use property store
- const properties = usePropertyStore(state => state.properties)
- const activeProperty = usePropertyStore(state => state.activeProperty)
- const isLoading = usePropertyStore(state => state.isLoading)
- const setActiveProperty = usePropertyStore(state => state.setActiveProperty)
- const fetchProperties = usePropertyStore(state => state.fetchProperties)
-
- const [isNewPropertyDialogOpen, setIsNewPropertyDialogOpen] = useState(false)
-
- const handlePropertySelect = async (propertyId: string) => {
- await setActiveProperty(propertyId)
- }
-
- const handleAddNew = () => {
- setIsNewPropertyDialogOpen(true)
- }
-
- const handlePropertyCreated = async (propertyId: string) => {
- // Set the newly created property as active (this will also fetch properties)
- await setActiveProperty(propertyId)
- }
-
- return (
- <>
-
-
-
-
-
- {/* Property list */}
- {properties.length > 0 ? (
-
- {properties.map((property) => (
-
- activeProperty?.id === property.id ? null : handlePropertySelect(property.id)
- }
- >
-
-
{property.name}
- {activeProperty?.id === property.id && (
-
- )}
-
-
- ))}
-
- ) : (
-
- No properties yet
-
- )}
-
- {/* Add new property option */}
-
-
- Add new property
-
-
-
-
-
- >
- )
-}
diff --git a/apps/editor/features/community/lib/local-storage/hooks.ts b/apps/editor/features/community/lib/local-storage/hooks.ts
index 1be68e5f..86fe56d0 100644
--- a/apps/editor/features/community/lib/local-storage/hooks.ts
+++ b/apps/editor/features/community/lib/local-storage/hooks.ts
@@ -4,34 +4,34 @@ import { type AnyNodeId, initSpatialGridSync, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { useEffect, useRef } from 'react'
import useEditor from '@/store/use-editor'
-import { getLocalProperty, updateLocalPropertyScene } from './property-store'
+import { getLocalProject, updateLocalProjectScene } from './project-store'
/**
- * Hook for local property scene management (guest users)
+ * Hook for local project scene management (guest users)
* Loads scene from localStorage and auto-saves changes
*/
-export function useLocalPropertyScene(propertyId?: string) {
+export function useLocalProjectScene(projectId?: string) {
const saveTimeoutRef = useRef
(undefined)
- const currentPropertyIdRef = useRef(null)
- const lastPropertyIdRef = useRef(null)
+ const currentProjectIdRef = useRef(null)
+ const lastProjectIdRef = useRef(null)
- // Load scene when property ID changes
+ // Load scene when project ID changes
useEffect(() => {
- if (!propertyId || !propertyId.startsWith('local_')) {
+ if (!projectId || !projectId.startsWith('local_')) {
return
}
- if (lastPropertyIdRef.current === propertyId) {
+ if (lastProjectIdRef.current === projectId) {
return
}
- lastPropertyIdRef.current = propertyId
- currentPropertyIdRef.current = propertyId
+ lastProjectIdRef.current = projectId
+ currentProjectIdRef.current = projectId
- const property = getLocalProperty(propertyId)
+ const project = getLocalProject(projectId)
- if (property?.scene_graph) {
- const { nodes, rootNodeIds } = property.scene_graph
+ if (project?.scene_graph) {
+ const { nodes, rootNodeIds } = project.scene_graph
useScene.getState().setScene(nodes, rootNodeIds as AnyNodeId[])
initSpatialGridSync()
} else {
@@ -45,16 +45,16 @@ export function useLocalPropertyScene(propertyId?: string) {
selectedIds: [],
zoneId: null,
})
- }, [propertyId])
+ }, [projectId])
// Auto-save to localStorage with debouncing
useEffect(() => {
- if (!propertyId || !propertyId.startsWith('local_')) {
- currentPropertyIdRef.current = null
+ if (!projectId || !projectId.startsWith('local_')) {
+ currentProjectIdRef.current = null
return
}
- currentPropertyIdRef.current = propertyId
+ currentProjectIdRef.current = projectId
let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
const unsubscribe = useScene.subscribe((state) => {
@@ -73,13 +73,13 @@ export function useLocalPropertyScene(propertyId?: string) {
// Debounce save by 1 second (faster than cloud save)
saveTimeoutRef.current = setTimeout(() => {
- const currentId = currentPropertyIdRef.current
+ const currentId = currentProjectIdRef.current
if (!currentId) return
const rootNodeIds = useScene.getState().rootNodeIds
const sceneGraph = { nodes, rootNodeIds }
- updateLocalPropertyScene(currentId, sceneGraph)
+ updateLocalProjectScene(currentId, sceneGraph)
}, 1000)
})
@@ -89,5 +89,5 @@ export function useLocalPropertyScene(propertyId?: string) {
}
unsubscribe()
}
- }, [propertyId])
+ }, [projectId])
}
diff --git a/apps/editor/features/community/lib/local-storage/project-store.ts b/apps/editor/features/community/lib/local-storage/project-store.ts
new file mode 100644
index 00000000..4560fb7d
--- /dev/null
+++ b/apps/editor/features/community/lib/local-storage/project-store.ts
@@ -0,0 +1,114 @@
+/**
+ * Local storage management for guest users
+ * Stores projects and scenes in browser localStorage
+ */
+
+import { createId } from '../utils/id-generator'
+
+export interface SceneGraph {
+ nodes: Record
+ rootNodeIds: string[]
+}
+
+export interface LocalProject {
+ id: string // Format: 'local_project_xyz'
+ name: string
+ created_at: string
+ updated_at: string
+ scene_graph: SceneGraph | null
+ is_local: true
+}
+
+const LOCAL_PROJECTS_KEY = 'pascal_local_projects'
+// Keep old key for migration
+const LEGACY_LOCAL_PROPERTIES_KEY = 'pascal_local_properties'
+
+function migrateLegacyStorage(): void {
+ if (typeof window === 'undefined') return
+
+ try {
+ const legacy = localStorage.getItem(LEGACY_LOCAL_PROPERTIES_KEY)
+ if (legacy && !localStorage.getItem(LOCAL_PROJECTS_KEY)) {
+ // Migrate old data to new key
+ const parsed = JSON.parse(legacy)
+ // Update IDs from local_property_* to local_project_*
+ const migrated = parsed.map((p: any) => ({
+ ...p,
+ id: p.id.replace('local_property_', 'local_project_'),
+ }))
+ localStorage.setItem(LOCAL_PROJECTS_KEY, JSON.stringify(migrated))
+ localStorage.removeItem(LEGACY_LOCAL_PROPERTIES_KEY)
+ }
+ } catch (error) {
+ console.error('Failed to migrate legacy local properties:', error)
+ }
+}
+
+export function getLocalProjects(): LocalProject[] {
+ if (typeof window === 'undefined') return []
+
+ migrateLegacyStorage()
+
+ try {
+ const stored = localStorage.getItem(LOCAL_PROJECTS_KEY)
+ return stored ? JSON.parse(stored) : []
+ } catch (error) {
+ console.error('Failed to load local projects:', error)
+ return []
+ }
+}
+
+export function getLocalProject(id: string): LocalProject | null {
+ const projects = getLocalProjects()
+ return projects.find((p) => p.id === id) || null
+}
+
+export function saveLocalProject(project: LocalProject): void {
+ const projects = getLocalProjects()
+ const index = projects.findIndex((p) => p.id === project.id)
+
+ if (index >= 0) {
+ projects[index] = { ...project, updated_at: new Date().toISOString() }
+ } else {
+ projects.push(project)
+ }
+
+ localStorage.setItem(LOCAL_PROJECTS_KEY, JSON.stringify(projects))
+}
+
+export function createLocalProject(name: string): LocalProject {
+ const project: LocalProject = {
+ id: createId('local_project'),
+ name,
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ scene_graph: null,
+ is_local: true,
+ }
+
+ saveLocalProject(project)
+ return project
+}
+
+export function deleteLocalProject(id: string): void {
+ const projects = getLocalProjects().filter((p) => p.id !== id)
+ localStorage.setItem(LOCAL_PROJECTS_KEY, JSON.stringify(projects))
+}
+
+export function updateLocalProjectScene(id: string, sceneGraph: SceneGraph): void {
+ const project = getLocalProject(id)
+ if (project) {
+ project.scene_graph = sceneGraph
+ saveLocalProject(project)
+ }
+}
+
+export function migrateLocalProjectsToCloud(userId: string): LocalProject[] {
+ // Return local projects that need to be migrated
+ // Actual migration handled by separate function
+ return getLocalProjects()
+}
+
+export function clearLocalProjects(): void {
+ localStorage.removeItem(LOCAL_PROJECTS_KEY)
+}
diff --git a/apps/editor/features/community/lib/local-storage/property-store.ts b/apps/editor/features/community/lib/local-storage/property-store.ts
deleted file mode 100644
index 3730054e..00000000
--- a/apps/editor/features/community/lib/local-storage/property-store.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * Local storage management for guest users
- * Stores properties and scenes in browser localStorage
- */
-
-import { createId } from '../utils/id-generator'
-
-export interface SceneGraph {
- nodes: Record
- rootNodeIds: string[]
-}
-
-export interface LocalProperty {
- id: string // Format: 'local_property_xyz'
- name: string
- created_at: string
- updated_at: string
- scene_graph: SceneGraph | null
- is_local: true
-}
-
-const LOCAL_PROPERTIES_KEY = 'pascal_local_properties'
-
-export function getLocalProperties(): LocalProperty[] {
- if (typeof window === 'undefined') return []
-
- try {
- const stored = localStorage.getItem(LOCAL_PROPERTIES_KEY)
- return stored ? JSON.parse(stored) : []
- } catch (error) {
- console.error('Failed to load local properties:', error)
- return []
- }
-}
-
-export function getLocalProperty(id: string): LocalProperty | null {
- const properties = getLocalProperties()
- return properties.find((p) => p.id === id) || null
-}
-
-export function saveLocalProperty(property: LocalProperty): void {
- const properties = getLocalProperties()
- const index = properties.findIndex((p) => p.id === property.id)
-
- if (index >= 0) {
- properties[index] = { ...property, updated_at: new Date().toISOString() }
- } else {
- properties.push(property)
- }
-
- localStorage.setItem(LOCAL_PROPERTIES_KEY, JSON.stringify(properties))
-}
-
-export function createLocalProperty(name: string): LocalProperty {
- const property: LocalProperty = {
- id: createId('local_property'),
- name,
- created_at: new Date().toISOString(),
- updated_at: new Date().toISOString(),
- scene_graph: null,
- is_local: true,
- }
-
- saveLocalProperty(property)
- return property
-}
-
-export function deleteLocalProperty(id: string): void {
- const properties = getLocalProperties().filter((p) => p.id !== id)
- localStorage.setItem(LOCAL_PROPERTIES_KEY, JSON.stringify(properties))
-}
-
-export function updateLocalPropertyScene(id: string, sceneGraph: SceneGraph): void {
- const property = getLocalProperty(id)
- if (property) {
- property.scene_graph = sceneGraph
- saveLocalProperty(property)
- }
-}
-
-export function migrateLocalPropertiesToCloud(userId: string): LocalProperty[] {
- // Return local properties that need to be migrated
- // Actual migration handled by separate function
- return getLocalProperties()
-}
-
-export function clearLocalProperties(): void {
- localStorage.removeItem(LOCAL_PROPERTIES_KEY)
-}
diff --git a/apps/editor/features/community/lib/models/actions.ts b/apps/editor/features/community/lib/models/actions.ts
index 1d0b5bcc..0aad71f4 100644
--- a/apps/editor/features/community/lib/models/actions.ts
+++ b/apps/editor/features/community/lib/models/actions.ts
@@ -1,6 +1,6 @@
/**
- * Property model actions - Server actions for scene loading/saving
- * Manages 3D models (scene graphs) stored in properties_models table
+ * Project model actions - Server actions for scene loading/saving
+ * Manages 3D models (scene graphs) stored in projects_models table
*/
'use server'
@@ -9,28 +9,28 @@ import type { AnyNode, AnyNodeId } from '@pascal-app/core'
import { createServerSupabaseClient } from '../database/server'
import { getSession } from '../auth/server'
import { createId } from '../utils/id-generator'
-import type { ActionResult } from '../properties/actions'
+import type { ActionResult } from '../projects/actions'
export interface SceneGraph {
nodes: Record
rootNodeIds: AnyNodeId[]
}
-export interface PropertyModel {
+export interface ProjectModel {
id: string
name: string
version: number
draft: boolean
- property_id: string
+ project_id: string
scene_graph: SceneGraph | null
created_at: string
updated_at: string
}
/**
- * Get the latest model for a property (highest version)
+ * Get the latest model for a project (highest version)
*/
-export async function getPropertyModel(propertyId: string): Promise> {
+export async function getProjectModel(projectId: string): Promise> {
try {
const session = await getSession()
@@ -44,23 +44,23 @@ export async function getPropertyModel(propertyId: string): Promise()
- if (propertyError || !property) {
+ if (projectError || !project) {
return {
success: false,
- error: 'Property not found',
+ error: 'Project not found',
data: null,
}
}
// Verify ownership
- if (property.owner_id !== session.user.id) {
+ if (project.owner_id !== session.user.id) {
return {
success: false,
error: 'Unauthorized',
@@ -70,17 +70,17 @@ export async function getPropertyModel(propertyId: string): Promise()
+ .single()
- console.log('[getPropertyModel] Query result:', {
- propertyId,
+ console.log('[getProjectModel] Query result:', {
+ projectId,
hasModel: !!model,
modelError: modelError?.message,
errorCode: modelError?.code,
@@ -90,14 +90,14 @@ export async function getPropertyModel(propertyId: string): Promise> {
+): Promise> {
try {
const session = await getSession()
@@ -144,22 +144,22 @@ export async function savePropertyModel(
const supabase = await createServerSupabaseClient()
- // Get the property to verify ownership
- const { data: property, error: propertyError } = await supabase
- .from('properties')
+ // Get the project to verify ownership
+ const { data: project, error: projectError } = await supabase
+ .from('projects')
.select('id, owner_id, name')
- .eq('id', propertyId)
+ .eq('id', projectId)
.single<{ id: string; owner_id: string; name: string }>()
- if (propertyError || !property) {
+ if (projectError || !project) {
return {
success: false,
- error: 'Property not found',
+ error: 'Project not found',
}
}
// Verify ownership
- if (property.owner_id !== session.user.id) {
+ if (project.owner_id !== session.user.id) {
return {
success: false,
error: 'Unauthorized',
@@ -168,9 +168,9 @@ export async function savePropertyModel(
// Check if a model already exists
const { data: existingModel } = await supabase
- .from('properties_models')
+ .from('projects_models')
.select('id, version')
- .eq('property_id', propertyId)
+ .eq('project_id', projectId)
.is('deleted_at', null)
.order('version', { ascending: false })
.order('created_at', { ascending: false })
@@ -184,11 +184,11 @@ export async function savePropertyModel(
updated_at: new Date().toISOString(),
}
const { data: updatedModel, error: updateError } = (await (supabase
- .from('properties_models') as any)
+ .from('projects_models') as any)
.update(updateData)
.eq('id', existingModel.id)
.select()
- .single()) as { data: PropertyModel | null; error: any }
+ .single()) as { data: ProjectModel | null; error: any }
if (updateError) {
return {
@@ -199,7 +199,7 @@ export async function savePropertyModel(
return {
success: true,
- data: updatedModel as PropertyModel,
+ data: updatedModel as ProjectModel,
message: 'Model updated successfully',
}
} else {
@@ -208,17 +208,17 @@ export async function savePropertyModel(
const insertData = {
id: modelId,
- property_id: propertyId,
- name: `${property.name} - Editor`,
+ project_id: projectId,
+ name: `${project.name} - Editor`,
version: 1,
draft: true,
scene_graph: sceneGraph,
}
const { data: newModel, error: createError } = (await (supabase
- .from('properties_models') as any)
+ .from('projects_models') as any)
.insert(insertData)
.select()
- .single()) as { data: PropertyModel | null; error: any }
+ .single()) as { data: ProjectModel | null; error: any }
if (createError) {
return {
@@ -229,14 +229,14 @@ export async function savePropertyModel(
return {
success: true,
- data: newModel as PropertyModel,
+ data: newModel as ProjectModel,
message: 'Model created successfully',
}
}
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : 'Failed to save property model',
+ error: error instanceof Error ? error.message : 'Failed to save project model',
}
}
}
diff --git a/apps/editor/features/community/lib/models/hooks.ts b/apps/editor/features/community/lib/models/hooks.ts
index 3109aeeb..a69c66a8 100644
--- a/apps/editor/features/community/lib/models/hooks.ts
+++ b/apps/editor/features/community/lib/models/hooks.ts
@@ -1,5 +1,5 @@
/**
- * Hooks for property model (scene) loading and auto-saving
+ * Hooks for project model (scene) loading and auto-saving
*/
'use client'
@@ -8,48 +8,48 @@ import { useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { useEffect, useRef } from 'react'
import useEditor from '@/store/use-editor'
-import { usePropertyStore } from '../properties/store'
-import { getPropertyModel, savePropertyModel } from './actions'
+import { useProjectStore } from '../projects/store'
+import { getProjectModel, saveProjectModel } from './actions'
/**
- * Load the scene when a property becomes active
+ * Load the scene when a project becomes active
* Saves changes automatically with debouncing
*/
-export function usePropertyScene() {
- // Subscribe to property store
- const activeProperty = usePropertyStore((state) => state.activeProperty)
- const isLoadingProperty = usePropertyStore((state) => state.isLoading)
+export function useProjectScene() {
+ // Subscribe to project store
+ const activeProject = useProjectStore((state) => state.activeProject)
+ const isLoadingProject = useProjectStore((state) => state.isLoading)
- const lastPropertyIdRef = useRef(null)
+ const lastProjectIdRef = useRef(null)
const saveTimeoutRef = useRef(undefined)
const isSavingRef = useRef(false)
- const currentPropertyIdRef = useRef(null)
+ const currentProjectIdRef = useRef(null)
- // Extract property ID for dependency tracking
- const propertyId = activeProperty?.id ?? null
- const propertyName = activeProperty?.name ?? null
+ // Extract project ID for dependency tracking
+ const projectId = activeProject?.id ?? null
+ const projectName = activeProject?.name ?? null
- // Load scene when active property changes
+ // Load scene when active project changes
useEffect(() => {
- if (isLoadingProperty) {
+ if (isLoadingProject) {
return
}
- if (!propertyId) {
+ if (!projectId) {
return
}
- // Skip if same property
- if (lastPropertyIdRef.current === propertyId) {
+ // Skip if same project
+ if (lastProjectIdRef.current === projectId) {
return
}
- lastPropertyIdRef.current = propertyId
+ lastProjectIdRef.current = projectId
- // Load the property's scene
+ // Load the project's scene
async function loadScene() {
try {
- const result = await getPropertyModel(propertyId || '')
+ const result = await getProjectModel(projectId || '')
if (result.success && result.data?.scene_graph) {
// Load the scene graph into the store
@@ -75,16 +75,16 @@ export function usePropertyScene() {
}
loadScene()
- }, [propertyId, isLoadingProperty])
+ }, [projectId, isLoadingProject])
// Auto-save scene changes with debouncing
useEffect(() => {
- if (!propertyId) {
- currentPropertyIdRef.current = null
+ if (!projectId) {
+ currentProjectIdRef.current = null
return
}
- currentPropertyIdRef.current = propertyId
+ currentProjectIdRef.current = projectId
// Subscribe to any scene changes
// Use JSON stringification to detect any node changes, not just count
@@ -113,9 +113,9 @@ export function usePropertyScene() {
// Debounce save by 2 seconds
saveTimeoutRef.current = setTimeout(async () => {
- // Get the current property ID at save time (not the captured value)
- const currentPropertyId = currentPropertyIdRef.current
- if (!currentPropertyId) {
+ // Get the current project ID at save time (not the captured value)
+ const currentProjectId = currentProjectIdRef.current
+ if (!currentProjectId) {
return
}
@@ -125,7 +125,7 @@ export function usePropertyScene() {
isSavingRef.current = true
try {
- await savePropertyModel(currentPropertyId, sceneGraph)
+ await saveProjectModel(currentProjectId, sceneGraph)
} finally {
isSavingRef.current = false
}
@@ -138,5 +138,5 @@ export function usePropertyScene() {
}
unsubscribe()
}
- }, [propertyId])
+ }, [projectId])
}
diff --git a/apps/editor/features/community/lib/properties/actions.ts b/apps/editor/features/community/lib/projects/actions.ts
similarity index 60%
rename from apps/editor/features/community/lib/properties/actions.ts
rename to apps/editor/features/community/lib/projects/actions.ts
index 4dd67d57..5a62a380 100644
--- a/apps/editor/features/community/lib/properties/actions.ts
+++ b/apps/editor/features/community/lib/projects/actions.ts
@@ -1,5 +1,5 @@
/**
- * Property actions - Server actions for property management
+ * Project actions - Server actions for project management
* Uses Better Auth session + Supabase to query the same database as the monorepo
*/
@@ -8,7 +8,7 @@
import { createServerSupabaseClient } from '../database/server'
import { getSession } from '../auth/server'
import { createId } from '../utils/id-generator'
-import type { CreatePropertyParams, Property, Database } from './types'
+import type { CreateProjectParams, Project, Database } from './types'
export type ActionResult = {
success: boolean
@@ -18,9 +18,9 @@ export type ActionResult = {
}
/**
- * Fetch all properties for the current user
+ * Fetch all projects for the current user
*/
-export async function getUserProperties(): Promise> {
+export async function getUserProjects(): Promise> {
try {
const session = await getSession()
@@ -34,12 +34,12 @@ export async function getUserProperties(): Promise> {
const supabase = await createServerSupabaseClient()
- // Query properties table with address relation
+ // Query projects table with address relation
const { data, error } = await supabase
- .from('properties')
+ .from('projects')
.select(`
*,
- address:properties_addresses(*)
+ address:projects_addresses(*)
`)
.eq('owner_id', session.user.id)
.order('created_at', { ascending: false })
@@ -54,21 +54,21 @@ export async function getUserProperties(): Promise> {
return {
success: true,
- data: data as Property[],
+ data: data as Project[],
}
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : 'Failed to fetch properties',
+ error: error instanceof Error ? error.message : 'Failed to fetch projects',
data: [],
}
}
}
/**
- * Get a specific property by ID for the current user
+ * Get a specific project by ID for the current user
*/
-export async function getPropertyById(propertyId: string): Promise> {
+export async function getProjectById(projectId: string): Promise> {
try {
const session = await getSession()
@@ -79,30 +79,30 @@ export async function getPropertyById(propertyId: string): Promise()
+ .single()
if (error) {
return { success: false, error: error.message, data: null }
}
- return { success: true, data: data as Property }
+ return { success: true, data: data as Project }
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : 'Failed to fetch property',
+ error: error instanceof Error ? error.message : 'Failed to fetch project',
data: null,
}
}
}
/**
- * Get the active property for the current session
+ * Get the active project for the current session
*/
-export async function getActiveProperty(): Promise> {
+export async function getActiveProject(): Promise> {
try {
const session = await getSession()
@@ -116,29 +116,29 @@ export async function getActiveProperty(): Promise
const supabase = await createServerSupabaseClient()
- // Get session's active_property_id from sessions table
+ // Get session's active_project_id from sessions table
const { data: sessionData, error: sessionError } = await supabase
.from('auth_sessions')
- .select('active_property_id')
+ .select('active_project_id')
.eq('user_id', session.user.id)
- .single<{ active_property_id: string | null }>()
+ .single<{ active_project_id: string | null }>()
- if (sessionError || !sessionData?.active_property_id) {
+ if (sessionError || !sessionData?.active_project_id) {
return {
success: true,
data: null,
}
}
- // Get the property with address
+ // Get the project with address
const { data, error } = await supabase
- .from('properties')
+ .from('projects')
.select(`
*,
- address:properties_addresses(*)
+ address:projects_addresses(*)
`)
- .eq('id', sessionData.active_property_id)
- .single()
+ .eq('id', sessionData.active_project_id)
+ .single()
if (error) {
return {
@@ -150,21 +150,21 @@ export async function getActiveProperty(): Promise
return {
success: true,
- data: data as Property,
+ data: data as Project,
}
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : 'Failed to fetch active property',
+ error: error instanceof Error ? error.message : 'Failed to fetch active project',
data: null,
}
}
}
/**
- * Set the active property for the current session
+ * Set the active project for the current session
*/
-export async function setActiveProperty(propertyId: string | null): Promise {
+export async function setActiveProject(projectId: string | null): Promise {
try {
const session = await getSession()
@@ -177,10 +177,10 @@ export async function setActiveProperty(propertyId: string | null): Promise> {
+export async function createProject(params: CreateProjectParams): Promise> {
try {
const session = await getSession()
@@ -218,55 +218,62 @@ export async function createProperty(params: CreatePropertyParams): Promise
> {
try {
@@ -332,12 +339,12 @@ export async function checkPropertyDuplicate(params: {
const supabase = await createServerSupabaseClient()
- // Query for existing property with matching address
+ // Query for existing project with matching address
const { data, error } = await supabase
- .from('properties')
+ .from('projects')
.select(`
*,
- address:properties_addresses!inner(*)
+ address:projects_addresses!inner(*)
`)
.eq('address.street_number', params.streetNumber || '')
.eq('address.route', params.route || '')
@@ -354,13 +361,13 @@ export async function checkPropertyDuplicate(params: {
}
if (data && data.length > 0) {
- const existingProperty = data[0] as unknown as Property
+ const existingProject = data[0] as unknown as Project
return {
success: true,
data: {
isDuplicate: true,
- isUserProperty: existingProperty.owner_id === session.user.id,
- existingProperty,
+ isUserProject: existingProject.owner_id === session.user.id,
+ existingProject,
},
}
}
@@ -380,17 +387,17 @@ export async function checkPropertyDuplicate(params: {
}
/**
- * Fetch public properties for community hub
+ * Fetch public projects for community hub
*/
-export async function getPublicProperties(): Promise> {
+export async function getPublicProjects(): Promise> {
try {
const supabase = await createServerSupabaseClient()
const { data, error } = await supabase
- .from('properties')
+ .from('projects')
.select(`
*,
- address:properties_addresses(*)
+ address:projects_addresses(*)
`)
.eq('is_private', false)
.order('views', { ascending: false })
@@ -406,65 +413,65 @@ export async function getPublicProperties(): Promise> {
return {
success: true,
- data: data as Property[],
+ data: data as Project[],
}
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : 'Failed to fetch public properties',
+ error: error instanceof Error ? error.message : 'Failed to fetch public projects',
data: [],
}
}
}
/**
- * Get a property model for viewing
- * Allows viewing if: property is public OR user owns the property
+ * Get a project model for viewing
+ * Allows viewing if: project is public OR user owns the project
*/
-export async function getPropertyModelPublic(propertyId: string): Promise<
- ActionResult<{ property: Property; model: any | null }>
+export async function getProjectModelPublic(projectId: string): Promise<
+ ActionResult<{ project: Project; model: any | null }>
> {
try {
const session = await getSession()
const supabase = await createServerSupabaseClient()
- // Get the property (without privacy filter first)
- const { data: property, error: propertyError } = await supabase
- .from('properties')
+ // Get the project (without privacy filter first)
+ const { data: project, error: projectError } = await supabase
+ .from('projects')
.select(`
*,
- address:properties_addresses(*)
+ address:projects_addresses(*)
`)
- .eq('id', propertyId)
+ .eq('id', projectId)
.single()
- if (propertyError || !property) {
+ if (projectError || !project) {
return {
success: false,
- error: 'Property not found',
+ error: 'Project not found',
data: undefined,
}
}
- // Check if user can view this property
- // Allow if: property is public OR user owns it
- const propertyData = property as any
- const isOwner = session?.user && propertyData.owner_id === session.user.id
- const isPublic = propertyData.is_private === false
+ // Check if user can view this project
+ // Allow if: project is public OR user owns it
+ const projectData = project as any
+ const isOwner = session?.user && projectData.owner_id === session.user.id
+ const isPublic = projectData.is_private === false
if (!isPublic && !isOwner) {
return {
success: false,
- error: 'Property is private',
+ error: 'Project is private',
data: undefined,
}
}
// Get the model
const { data: model } = await supabase
- .from('properties_models')
+ .from('projects_models')
.select('*')
- .eq('property_id', propertyId)
+ .eq('project_id', projectId)
.is('deleted_at', null)
.order('version', { ascending: false })
.limit(1)
@@ -473,28 +480,28 @@ export async function getPropertyModelPublic(propertyId: string): Promise<
return {
success: true,
data: {
- property: propertyData as Property,
+ project: projectData as Project,
model: model || null,
},
}
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : 'Failed to fetch property',
+ error: error instanceof Error ? error.message : 'Failed to fetch project',
data: undefined,
}
}
}
/**
- * Increment property view count
+ * Increment project view count
*/
-export async function incrementPropertyViews(propertyId: string): Promise {
+export async function incrementProjectViews(projectId: string): Promise {
try {
const supabase = await createServerSupabaseClient()
- const { error } = await supabase.rpc('increment_property_views', {
- property_id: propertyId,
+ const { error } = await supabase.rpc('increment_project_views', {
+ project_id: projectId,
} as any)
if (error) {
@@ -510,10 +517,10 @@ export async function incrementPropertyViews(propertyId: string): Promise {
try {
@@ -529,13 +536,13 @@ export async function updatePropertyPrivacy(
const supabase = await createServerSupabaseClient()
// Verify ownership
- const { data: property } = await supabase
- .from('properties')
+ const { data: project } = await supabase
+ .from('projects')
.select('owner_id')
- .eq('id', propertyId)
+ .eq('id', projectId)
.single()
- if ((property as any)?.owner_id !== session.user.id) {
+ if ((project as any)?.owner_id !== session.user.id) {
return {
success: false,
error: 'Unauthorized',
@@ -544,9 +551,9 @@ export async function updatePropertyPrivacy(
// Update privacy
const { error } = await (supabase
- .from('properties') as any)
+ .from('projects') as any)
.update({ is_private: isPrivate })
- .eq('id', propertyId)
+ .eq('id', projectId)
if (error) {
return {
@@ -557,21 +564,21 @@ export async function updatePropertyPrivacy(
return {
success: true,
- message: `Property is now ${isPrivate ? 'private' : 'public'}`,
+ message: `Project is now ${isPrivate ? 'private' : 'public'}`,
}
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : 'Failed to update property privacy',
+ error: error instanceof Error ? error.message : 'Failed to update project privacy',
}
}
}
/**
- * Update property address
+ * Update project address
*/
-export async function updatePropertyAddress(
- propertyId: string,
+export async function updateProjectAddress(
+ projectId: string,
addressData: {
street_number?: string
route?: string
@@ -594,20 +601,20 @@ export async function updatePropertyAddress(
const supabase = await createServerSupabaseClient()
// Verify ownership and get address_id
- const { data: property } = await supabase
- .from('properties')
+ const { data: project } = await supabase
+ .from('projects')
.select('owner_id, address_id')
- .eq('id', propertyId)
+ .eq('id', projectId)
.single()
- if (!property) {
+ if (!project) {
return {
success: false,
- error: 'Property not found',
+ error: 'Project not found',
}
}
- if ((property as any).owner_id !== session.user.id) {
+ if ((project as any).owner_id !== session.user.id) {
return {
success: false,
error: 'Unauthorized',
@@ -616,9 +623,9 @@ export async function updatePropertyAddress(
// Update address
const { error } = await (supabase
- .from('properties_addresses') as any)
+ .from('projects_addresses') as any)
.update(addressData)
- .eq('id', (property as any).address_id)
+ .eq('id', (project as any).address_id)
if (error) {
return {
@@ -640,11 +647,11 @@ export async function updatePropertyAddress(
}
/**
- * Migrate a local property to the cloud
- * Creates a new property with the local property's data
+ * Migrate a local project to the cloud
+ * Creates a new project with the local project's data
*/
-export async function migrateLocalProperty(
- localProperty: {
+export async function migrateLocalProject(
+ localProject: {
name: string
scene_graph: any
},
@@ -661,45 +668,31 @@ export async function migrateLocalProperty(
const supabase = await createServerSupabaseClient()
- // Create a default address (user can edit later via settings)
- const addressId = createId('address')
- const { error: addressError } = await (supabase.from('properties_addresses') as any).insert({
- id: addressId,
- country: 'US',
- })
-
- if (addressError) {
- return {
- success: false,
- error: addressError.message,
- }
- }
-
- // Create the property
- const propertyId = createId('property')
- const { error: propertyError } = await (supabase.from('properties') as any).insert({
- id: propertyId,
- name: localProperty.name,
+ // Create the project without an address (user can add one later via settings)
+ const projectId = createId('project')
+ const { error: projectError } = await (supabase.from('projects') as any).insert({
+ id: projectId,
+ name: localProject.name,
owner_id: session.user.id,
- address_id: addressId,
+ address_id: null,
is_private: true, // Default to private
})
- if (propertyError) {
+ if (projectError) {
return {
success: false,
- error: propertyError.message,
+ error: projectError.message,
}
}
// Create the model with the scene graph
- if (localProperty.scene_graph) {
+ if (localProject.scene_graph) {
const modelId = createId('model')
- const { error: modelError } = await (supabase.from('properties_models') as any).insert({
+ const { error: modelError } = await (supabase.from('projects_models') as any).insert({
id: modelId,
- property_id: propertyId,
+ project_id: projectId,
version: 1,
- scene_graph: localProperty.scene_graph,
+ scene_graph: localProject.scene_graph,
})
if (modelError) {
@@ -712,22 +705,22 @@ export async function migrateLocalProperty(
return {
success: true,
- data: { id: propertyId },
- message: 'Property migrated successfully',
+ data: { id: projectId },
+ message: 'Project migrated successfully',
}
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : 'Failed to migrate property',
+ error: error instanceof Error ? error.message : 'Failed to migrate project',
}
}
}
/**
- * Delete a property
- * Only the owner can delete their property
+ * Delete a project
+ * Only the owner can delete their project
*/
-export async function deleteProperty(propertyId: string): Promise {
+export async function deleteProject(projectId: string): Promise {
try {
const session = await getSession()
@@ -741,28 +734,28 @@ export async function deleteProperty(propertyId: string): Promise
const supabase = await createServerSupabaseClient()
// Verify ownership
- const { data: property } = await supabase
- .from('properties')
+ const { data: project } = await supabase
+ .from('projects')
.select('owner_id')
- .eq('id', propertyId)
+ .eq('id', projectId)
.single()
- if (!property) {
+ if (!project) {
return {
success: false,
- error: 'Property not found',
+ error: 'Project not found',
}
}
- if ((property as any).owner_id !== session.user.id) {
+ if ((project as any).owner_id !== session.user.id) {
return {
success: false,
error: 'Unauthorized',
}
}
- // Delete the property (cascade will delete related records)
- const { error } = await supabase.from('properties').delete().eq('id', propertyId)
+ // Delete the project (cascade will delete related records)
+ const { error } = await supabase.from('projects').delete().eq('id', projectId)
if (error) {
return {
@@ -773,28 +766,28 @@ export async function deleteProperty(propertyId: string): Promise
return {
success: true,
- message: 'Property deleted successfully',
+ message: 'Project deleted successfully',
}
} catch (error) {
return {
success: false,
- error: error instanceof Error ? error.message : 'Failed to delete property',
+ error: error instanceof Error ? error.message : 'Failed to delete project',
}
}
}
/**
- * Check if the current user has liked specific properties
- * Returns a map of propertyId -> boolean
+ * Check if the current user has liked specific projects
+ * Returns a map of projectId -> boolean
*/
-export async function getUserPropertyLikes(
- propertyIds: string[],
+export async function getUserProjectLikes(
+ projectIds: string[],
): Promise>> {
try {
const session = await getSession()
- if (!session?.user || propertyIds.length === 0) {
- // Return empty map for unauthenticated users or no properties
+ if (!session?.user || projectIds.length === 0) {
+ // Return empty map for unauthenticated users or no projects
return {
success: true,
data: {},
@@ -804,10 +797,10 @@ export async function getUserPropertyLikes(
const supabase = await createServerSupabaseClient()
const { data: likes, error } = await supabase
- .from('property_likes')
- .select('property_id')
+ .from('projects_likes')
+ .select('project_id')
.eq('user_id', session.user.id)
- .in('property_id', propertyIds)
+ .in('project_id', projectIds)
if (error) {
return {
@@ -819,8 +812,8 @@ export async function getUserPropertyLikes(
// Convert array to map
const likeMap: Record = {}
- propertyIds.forEach((id) => {
- likeMap[id] = likes?.some((like) => (like as any).property_id === id) || false
+ projectIds.forEach((id) => {
+ likeMap[id] = likes?.some((like) => (like as any).project_id === id) || false
})
return {
@@ -837,11 +830,11 @@ export async function getUserPropertyLikes(
}
/**
- * Toggle like on a property
+ * Toggle like on a project
* Returns the new like state and updated like count
*/
-export async function togglePropertyLike(
- propertyId: string,
+export async function toggleProjectLike(
+ projectId: string,
): Promise> {
try {
const session = await getSession()
@@ -856,11 +849,11 @@ export async function togglePropertyLike(
const supabase = await createServerSupabaseClient()
const userId = session.user.id
- // Check if user has already liked this property
+ // Check if user has already liked this project
const { data: existingLike } = await supabase
- .from('property_likes')
+ .from('projects_likes')
.select('id')
- .eq('property_id', propertyId)
+ .eq('project_id', projectId)
.eq('user_id', userId)
.maybeSingle()
@@ -869,7 +862,7 @@ export async function togglePropertyLike(
if (existingLike) {
// Unlike - remove the like
const { error } = await (supabase
- .from('property_likes') as any)
+ .from('projects_likes') as any)
.delete()
.eq('id', (existingLike as any).id)
@@ -884,9 +877,9 @@ export async function togglePropertyLike(
} else {
// Like - add a new like
const likeId = createId('like')
- const { error } = await (supabase.from('property_likes') as any).insert({
+ const { error } = await (supabase.from('projects_likes') as any).insert({
id: likeId,
- property_id: propertyId,
+ project_id: projectId,
user_id: userId,
})
@@ -901,15 +894,15 @@ export async function togglePropertyLike(
}
// Get updated like count
- const { data: likeCount } = await supabase.rpc('get_property_like_count', {
- property_id: propertyId,
+ const { data: likeCount } = await supabase.rpc('get_project_like_count', {
+ project_id: projectId,
} as any)
- // Update the property's like count cache
+ // Update the project's like count cache
await (supabase
- .from('properties') as any)
+ .from('projects') as any)
.update({ likes: likeCount || 0 })
- .eq('id', propertyId)
+ .eq('id', projectId)
return {
success: true,
@@ -927,10 +920,10 @@ export async function togglePropertyLike(
}
/**
- * Upload a thumbnail image to Supabase Storage and update the property
+ * Upload a thumbnail image to Supabase Storage and update the project
*/
-export async function uploadPropertyThumbnail(
- propertyId: string,
+export async function uploadProjectThumbnail(
+ projectId: string,
blob: Blob
): Promise<{ success: true; data: { thumbnail_url: string } } | { success: false; error: string }> {
try {
@@ -947,28 +940,28 @@ export async function uploadPropertyThumbnail(
const supabase = await createServerSupabaseClient()
- // Verify the user owns this property
- const { data: property, error: propertyError } = await supabase
- .from('properties')
+ // Verify the user owns this project
+ const { data: project, error: projectError } = await supabase
+ .from('projects')
.select('owner_id')
- .eq('id', propertyId)
+ .eq('id', projectId)
.single()
- if (propertyError || !property) {
- return { success: false, error: 'Property not found' }
+ if (projectError || !project) {
+ return { success: false, error: 'Project not found' }
}
- if ((property as any).owner_id !== session.user.id) {
- return { success: false, error: 'Not authorized to update this property' }
+ if ((project as any).owner_id !== session.user.id) {
+ return { success: false, error: 'Not authorized to update this project' }
}
// Generate a unique filename
const timestamp = Date.now()
- const filename = `${propertyId}/${timestamp}.png`
+ const filename = `${projectId}/${timestamp}.png`
// Upload to Supabase Storage
const { data: uploadData, error: uploadError } = await supabase.storage
- .from('property-thumbnails')
+ .from('project-thumbnails')
.upload(filename, blob, {
contentType: 'image/png',
upsert: false,
@@ -980,19 +973,19 @@ export async function uploadPropertyThumbnail(
// Get the public URL
const { data: urlData } = supabase.storage
- .from('property-thumbnails')
+ .from('project-thumbnails')
.getPublicUrl(uploadData.path)
const thumbnailUrl = urlData.publicUrl
- // Update the property with the new thumbnail URL
+ // Update the project with the new thumbnail URL
const { error: updateError } = await (supabase
- .from('properties') as any)
+ .from('projects') as any)
.update({ thumbnail_url: thumbnailUrl })
- .eq('id', propertyId)
+ .eq('id', projectId)
if (updateError) {
- return { success: false, error: `Failed to update property: ${updateError.message}` }
+ return { success: false, error: `Failed to update project: ${updateError.message}` }
}
return {
diff --git a/apps/editor/features/community/lib/projects/store.ts b/apps/editor/features/community/lib/projects/store.ts
new file mode 100644
index 00000000..afe83172
--- /dev/null
+++ b/apps/editor/features/community/lib/projects/store.ts
@@ -0,0 +1,89 @@
+/**
+ * Project store - Zustand store for project state management
+ */
+
+import { create } from 'zustand'
+import type { Project } from './types'
+import {
+ getActiveProject,
+ getUserProjects,
+ getProjectById,
+} from './actions'
+
+interface ProjectStore {
+ // State
+ activeProject: Project | null
+ projects: Project[]
+ isLoading: boolean
+ error: string | null
+
+ // Actions
+ fetchProjects: () => Promise
+ fetchActiveProject: () => Promise
+ setActiveProject: (projectId: string) => Promise
+ initialize: () => Promise
+}
+
+export const useProjectStore = create((set, get) => ({
+ // Initial state
+ activeProject: null,
+ projects: [],
+ isLoading: true,
+ error: null,
+
+ // Fetch all projects
+ fetchProjects: async () => {
+ const result = await getUserProjects()
+
+ if (result.success) {
+ set({ projects: result.data || [], error: null })
+ } else {
+ set({ error: result.error || 'Failed to fetch projects', projects: [] })
+ }
+ },
+
+ // Fetch the active project from database
+ fetchActiveProject: async () => {
+ set({ isLoading: true })
+
+ const result = await getActiveProject()
+
+ if (result.success) {
+ set({
+ activeProject: result.data || null,
+ isLoading: false,
+ error: null
+ })
+ // Note: Auto-select logic removed - now using URL-based routing
+ // The URL parameter determines which project to load
+ } else {
+ set({
+ error: result.error || 'Failed to fetch active project',
+ activeProject: null,
+ isLoading: false
+ })
+ }
+ },
+
+ // Set active project by fetching it directly by ID (URL-based, no session update)
+ setActiveProject: async (projectId: string) => {
+ set({ isLoading: true })
+
+ const result = await getProjectById(projectId)
+
+ if (result.success && result.data) {
+ set({ activeProject: result.data, isLoading: false, error: null })
+ } else {
+ set({ isLoading: false, error: result.error || 'Project not found' })
+ }
+ },
+
+ // Initialize - fetch both projects and active project
+ initialize: async () => {
+ set({ isLoading: true })
+ await Promise.all([
+ get().fetchProjects(),
+ get().fetchActiveProject(),
+ ])
+ },
+}))
diff --git a/apps/editor/features/community/lib/properties/types.ts b/apps/editor/features/community/lib/projects/types.ts
similarity index 56%
rename from apps/editor/features/community/lib/properties/types.ts
rename to apps/editor/features/community/lib/projects/types.ts
index c2a9290f..db7e8862 100644
--- a/apps/editor/features/community/lib/properties/types.ts
+++ b/apps/editor/features/community/lib/projects/types.ts
@@ -1,15 +1,15 @@
/**
- * Property-related type definitions
+ * Project-related type definitions
* Isolated from monorepo database schema
*/
// Database table row types
-export type DbProperty = {
+export type DbProject = {
id: string
name: string
owner_id: string
organization_id: string | null
- address_id: string
+ address_id: string | null
created_at: string
updated_at: string
is_private: boolean
@@ -18,7 +18,7 @@ export type DbProperty = {
thumbnail_url: string | null
}
-export type DbPropertyAddress = {
+export type DbProjectAddress = {
id: string
street_number?: string
route?: string
@@ -32,9 +32,9 @@ export type DbPropertyAddress = {
updated_at: string
}
-export type DbPropertyModel = {
+export type DbProjectModel = {
id: string
- property_id: string
+ project_id: string
version: number
scene_graph: any
created_at: string
@@ -42,9 +42,9 @@ export type DbPropertyModel = {
deleted_at: string | null
}
-export type DbPropertyLike = {
+export type DbProjectLike = {
id: string
- property_id: string
+ project_id: string
user_id: string
created_at: string
}
@@ -53,46 +53,46 @@ export type DbPropertyLike = {
export type Database = {
public: {
Tables: {
- properties: {
- Row: DbProperty
- Insert: Omit
- Update: Partial>
+ projects: {
+ Row: DbProject
+ Insert: Omit
+ Update: Partial>
}
- properties_addresses: {
- Row: DbPropertyAddress
- Insert: Omit
- Update: Partial>
+ projects_addresses: {
+ Row: DbProjectAddress
+ Insert: Omit
+ Update: Partial>
}
- properties_models: {
- Row: DbPropertyModel
- Insert: Omit
- Update: Partial>
+ projects_models: {
+ Row: DbProjectModel
+ Insert: Omit
+ Update: Partial>
}
- property_likes: {
- Row: DbPropertyLike
- Insert: Omit
- Update: Partial>
+ projects_likes: {
+ Row: DbProjectLike
+ Insert: Omit
+ Update: Partial>
}
}
Functions: {
- increment_property_views: {
- Args: { property_id: string }
+ increment_project_views: {
+ Args: { project_id: string }
Returns: undefined
}
- get_property_like_count: {
- Args: { property_id: string }
+ get_project_like_count: {
+ Args: { project_id: string }
Returns: number
}
}
}
}
-export type Property = {
+export type Project = {
id: string
name: string
owner_id: string
organization_id: string | null
- address_id: string
+ address_id: string | null
created_at: string
updated_at: string
// Community features
@@ -110,12 +110,12 @@ export type Property = {
country?: string
latitude?: string
longitude?: string
- }
+ } | null
}
-export type CreatePropertyParams = {
+export type CreateProjectParams = {
name: string
- center: [number, number]
+ center?: [number, number]
streetNumber?: string
route?: string
routeShort?: string
diff --git a/apps/editor/features/community/lib/properties/store.ts b/apps/editor/features/community/lib/properties/store.ts
deleted file mode 100644
index b24b0e19..00000000
--- a/apps/editor/features/community/lib/properties/store.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * Property store - Zustand store for property state management
- */
-
-import { create } from 'zustand'
-import type { Property } from './types'
-import {
- getActiveProperty,
- getUserProperties,
- getPropertyById,
-} from './actions'
-
-interface PropertyStore {
- // State
- activeProperty: Property | null
- properties: Property[]
- isLoading: boolean
- error: string | null
-
- // Actions
- fetchProperties: () => Promise
- fetchActiveProperty: () => Promise
- setActiveProperty: (propertyId: string) => Promise
- initialize: () => Promise
-}
-
-export const usePropertyStore = create((set, get) => ({
- // Initial state
- activeProperty: null,
- properties: [],
- isLoading: true,
- error: null,
-
- // Fetch all properties
- fetchProperties: async () => {
- const result = await getUserProperties()
-
- if (result.success) {
- set({ properties: result.data || [], error: null })
- } else {
- set({ error: result.error || 'Failed to fetch properties', properties: [] })
- }
- },
-
- // Fetch the active property from database
- fetchActiveProperty: async () => {
- set({ isLoading: true })
-
- const result = await getActiveProperty()
-
- if (result.success) {
- set({
- activeProperty: result.data || null,
- isLoading: false,
- error: null
- })
- // Note: Auto-select logic removed - now using URL-based routing
- // The URL parameter determines which property to load
- } else {
- set({
- error: result.error || 'Failed to fetch active property',
- activeProperty: null,
- isLoading: false
- })
- }
- },
-
- // Set active property by fetching it directly by ID (URL-based, no session update)
- setActiveProperty: async (propertyId: string) => {
- set({ isLoading: true })
-
- const result = await getPropertyById(propertyId)
-
- if (result.success && result.data) {
- set({ activeProperty: result.data, isLoading: false, error: null })
- } else {
- set({ isLoading: false, error: result.error || 'Property not found' })
- }
- },
-
- // Initialize - fetch both properties and active property
- initialize: async () => {
- set({ isLoading: true })
- await Promise.all([
- get().fetchProperties(),
- get().fetchActiveProperty(),
- ])
- },
-}))
diff --git a/apps/editor/lib/navigation.ts b/apps/editor/lib/navigation.ts
index dbb7b2ad..51a9a5d1 100644
--- a/apps/editor/lib/navigation.ts
+++ b/apps/editor/lib/navigation.ts
@@ -1,13 +1,13 @@
/**
- * Navigation helpers for property-based routing
+ * Navigation helpers for project-based routing
*/
-export function getEditorUrl(propertyId: string): string {
- return `/editor/${propertyId}`
+export function getEditorUrl(projectId: string): string {
+ return `/editor/${projectId}`
}
-export function getViewerUrl(propertyId: string): string {
- return `/viewer/${propertyId}`
+export function getViewerUrl(projectId: string): string {
+ return `/viewer/${projectId}`
}
export function getHomeUrl(): string {
diff --git a/packages/core/src/events/bus.ts b/packages/core/src/events/bus.ts
index 3cd26468..7c73b92e 100644
--- a/packages/core/src/events/bus.ts
+++ b/packages/core/src/events/bus.ts
@@ -56,7 +56,7 @@ export interface CameraControlEvent {
}
export interface ThumbnailGenerateEvent {
- propertyId: string
+ projectId: string
}
type CameraControlEvents = {
diff --git a/packages/db/src/schema/auth/sessions.ts b/packages/db/src/schema/auth/sessions.ts
index bfd83511..23d03b27 100644
--- a/packages/db/src/schema/auth/sessions.ts
+++ b/packages/db/src/schema/auth/sessions.ts
@@ -13,8 +13,8 @@ export const sessions = pgTable('auth_sessions', (t) => ({
token: t.text('token').notNull(),
ipAddress: t.text('ip_address'),
userAgent: t.text('user_agent'),
- // Custom: active property for the session context
- activePropertyId: t.text('active_property_id'),
+ // Custom: active project for the session context
+ activeProjectId: t.text('active_project_id'),
// Admin plugin support: tracks who is impersonating this session
impersonatedBy: t
.text('impersonated_by')
diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts
index 656cb454..4265c8bd 100644
--- a/packages/db/src/schema/index.ts
+++ b/packages/db/src/schema/index.ts
@@ -5,7 +5,8 @@ export * from './auth/sessions'
export * from './auth/users'
export * from './auth/verifications'
-// Property tables
-export * from './properties/addresses'
-export * from './properties/models'
-export * from './properties/properties'
+// Project tables
+export * from './projects/addresses'
+export * from './projects/likes'
+export * from './projects/models'
+export * from './projects/projects'
diff --git a/packages/db/src/schema/properties/addresses.ts b/packages/db/src/schema/projects/addresses.ts
similarity index 98%
rename from packages/db/src/schema/properties/addresses.ts
rename to packages/db/src/schema/projects/addresses.ts
index ea4925f3..bee8e057 100644
--- a/packages/db/src/schema/properties/addresses.ts
+++ b/packages/db/src/schema/projects/addresses.ts
@@ -3,7 +3,7 @@ import { z } from 'zod'
import { id, timestampsColumns } from '../../helpers'
export const addresses = pgTable(
- 'properties_addresses',
+ 'projects_addresses',
(t) => ({
id: id('address'),
streetNumber: t.text('street_number'),
diff --git a/packages/db/src/schema/projects/likes.ts b/packages/db/src/schema/projects/likes.ts
new file mode 100644
index 00000000..56a8e1c4
--- /dev/null
+++ b/packages/db/src/schema/projects/likes.ts
@@ -0,0 +1,36 @@
+import { relations } from 'drizzle-orm'
+import { pgTable, unique } from 'drizzle-orm/pg-core'
+import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
+import { id, createdAt } from '../../helpers'
+import { projects } from './projects'
+import { users } from '../auth/users'
+
+export const projectsLikes = pgTable(
+ 'projects_likes',
+ (t) => ({
+ id: id('like'),
+ projectId: t
+ .text('project_id')
+ .notNull()
+ .references(() => projects.id, { onDelete: 'cascade' }),
+ userId: t
+ .text('user_id')
+ .notNull(),
+ createdAt,
+ }),
+ (t) => [
+ unique('projects_likes_project_user_unique').on(t.projectId, t.userId),
+ ],
+).enableRLS()
+
+export const projectsLikesRelations = relations(projectsLikes, ({ one }) => ({
+ project: one(projects, {
+ fields: [projectsLikes.projectId],
+ references: [projects.id],
+ }),
+}))
+
+export type ProjectLike = typeof projectsLikes.$inferSelect
+export type NewProjectLike = typeof projectsLikes.$inferInsert
+export const insertProjectLikeSchema = createInsertSchema(projectsLikes)
+export const selectProjectLikeSchema = createSelectSchema(projectsLikes)
diff --git a/packages/db/src/schema/properties/models.ts b/packages/db/src/schema/projects/models.ts
similarity index 72%
rename from packages/db/src/schema/properties/models.ts
rename to packages/db/src/schema/projects/models.ts
index 2f6a51ab..0fb4775e 100644
--- a/packages/db/src/schema/properties/models.ts
+++ b/packages/db/src/schema/projects/models.ts
@@ -2,26 +2,26 @@ import { relations } from 'drizzle-orm'
import { pgTable } from 'drizzle-orm/pg-core'
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
import { id, timestampsColumnsSoftDelete } from '../../helpers'
-import { properties } from './properties'
+import { projects } from './projects'
-export const models = pgTable('properties_models', (t) => ({
+export const models = pgTable('projects_models', (t) => ({
id: id('model'),
name: t.text('name'),
version: t.integer('version').default(1),
description: t.text('description'),
draft: t.boolean('draft').default(true),
- propertyId: t
- .text('property_id')
- .references(() => properties.id, { onDelete: 'set null' }),
+ projectId: t
+ .text('project_id')
+ .references(() => projects.id, { onDelete: 'set null' }),
sceneGraph: t.jsonb('scene_graph'),
metadata: t.jsonb('metadata'),
...timestampsColumnsSoftDelete,
})).enableRLS()
export const modelsRelations = relations(models, ({ one }) => ({
- property: one(properties, {
- fields: [models.propertyId],
- references: [properties.id],
+ project: one(projects, {
+ fields: [models.projectId],
+ references: [projects.id],
}),
}))
diff --git a/packages/db/src/schema/properties/properties.ts b/packages/db/src/schema/projects/projects.ts
similarity index 55%
rename from packages/db/src/schema/properties/properties.ts
rename to packages/db/src/schema/projects/projects.ts
index 97eafbf0..b9ac6475 100644
--- a/packages/db/src/schema/properties/properties.ts
+++ b/packages/db/src/schema/projects/projects.ts
@@ -5,15 +5,14 @@ import { id, timestampsColumns } from '../../helpers'
import { users } from '../auth/users'
import { addresses } from './addresses'
-export const properties = pgTable(
- 'properties',
+export const projects = pgTable(
+ 'projects',
(t) => ({
- id: id('property'),
+ id: id('project'),
name: t.text('name'),
addressId: t
.text('address_id')
- .references(() => addresses.id, { onDelete: 'set null' })
- .unique(),
+ .references(() => addresses.id, { onDelete: 'set null' }),
ownerId: t
.text('owner_id')
.references(() => users.id, { onDelete: 'set null' }),
@@ -27,26 +26,26 @@ export const properties = pgTable(
...timestampsColumns,
}),
(t) => [
- index('property_address_idx').on(t.addressId),
- index('property_owner_idx').on(t.ownerId),
- index('property_is_private_idx').on(t.isPrivate),
- index('property_views_idx').on(t.views),
- index('property_likes_idx').on(t.likes),
+ index('project_address_idx').on(t.addressId),
+ index('project_owner_idx').on(t.ownerId),
+ index('project_is_private_idx').on(t.isPrivate),
+ index('project_views_idx').on(t.views),
+ index('project_likes_idx').on(t.likes),
],
).enableRLS()
-export const propertiesRelations = relations(properties, ({ one }) => ({
+export const projectsRelations = relations(projects, ({ one }) => ({
address: one(addresses, {
- fields: [properties.addressId],
+ fields: [projects.addressId],
references: [addresses.id],
}),
owner: one(users, {
- fields: [properties.ownerId],
+ fields: [projects.ownerId],
references: [users.id],
}),
}))
-export type Property = typeof properties.$inferSelect
-export type NewProperty = typeof properties.$inferInsert
-export const insertPropertySchema = createInsertSchema(properties)
-export const selectPropertySchema = createSelectSchema(properties)
+export type Project = typeof projects.$inferSelect
+export type NewProject = typeof projects.$inferInsert
+export const insertProjectSchema = createInsertSchema(projects)
+export const selectProjectSchema = createSelectSchema(projects)
diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts
index 7e246c17..bcab2216 100644
--- a/packages/db/src/types.ts
+++ b/packages/db/src/types.ts
@@ -8,7 +8,7 @@ export type Json = string | number | boolean | null | { [key: string]: Json | un
export interface Database {
public: {
Tables: {
- properties: {
+ projects: {
Row: {
id: string
name: string
@@ -31,10 +31,10 @@ export interface Database {
updated_at?: string
}
}
- properties_addresses: {
+ projects_addresses: {
Row: {
id: string
- property_id: string
+ project_id: string
formatted_address: string
street_number: string | null
route: string | null
@@ -51,7 +51,7 @@ export interface Database {
}
Insert: {
id?: string
- property_id: string
+ project_id: string
formatted_address: string
street_number?: string | null
route?: string | null
@@ -68,7 +68,7 @@ export interface Database {
}
Update: {
id?: string
- property_id?: string
+ project_id?: string
formatted_address?: string
street_number?: string | null
route?: string | null
@@ -84,10 +84,10 @@ export interface Database {
updated_at?: string
}
}
- properties_models: {
+ projects_models: {
Row: {
id: string
- property_id: string
+ project_id: string
name: string
version: number
draft: boolean
@@ -97,7 +97,7 @@ export interface Database {
}
Insert: {
id?: string
- property_id: string
+ project_id: string
name: string
version?: number
draft?: boolean
@@ -107,7 +107,7 @@ export interface Database {
}
Update: {
id?: string
- property_id?: string
+ project_id?: string
name?: string
version?: number
draft?: boolean
diff --git a/packages/db/supabase/migrations/20240217000001_rename_properties_to_projects.sql b/packages/db/supabase/migrations/20240217000001_rename_properties_to_projects.sql
new file mode 100644
index 00000000..c250cae2
--- /dev/null
+++ b/packages/db/supabase/migrations/20240217000001_rename_properties_to_projects.sql
@@ -0,0 +1,247 @@
+-- Migration: Rename properties β projects
+-- This renames all property-related tables, columns, indexes, policies, and functions.
+
+BEGIN;
+
+-- ============================================================
+-- 1. Drop existing RLS policies (they reference old table names)
+-- ============================================================
+
+-- properties policies
+DROP POLICY IF EXISTS "Users can view own or public properties" ON properties;
+DROP POLICY IF EXISTS "Users can insert their own properties" ON properties;
+DROP POLICY IF EXISTS "Users can update their own properties" ON properties;
+DROP POLICY IF EXISTS "Users can delete their own properties" ON properties;
+
+-- properties_addresses policies
+DROP POLICY IF EXISTS "Authenticated users can view all addresses" ON properties_addresses;
+DROP POLICY IF EXISTS "Authenticated users can insert addresses" ON properties_addresses;
+DROP POLICY IF EXISTS "Authenticated users can update addresses" ON properties_addresses;
+DROP POLICY IF EXISTS "Authenticated users can delete addresses" ON properties_addresses;
+
+-- properties_models policies
+DROP POLICY IF EXISTS "Users can view models of own or public properties" ON properties_models;
+DROP POLICY IF EXISTS "Users can insert models for their own properties" ON properties_models;
+DROP POLICY IF EXISTS "Users can update models of their own properties" ON properties_models;
+DROP POLICY IF EXISTS "Users can delete models of their own properties" ON properties_models;
+
+-- property_likes policies
+DROP POLICY IF EXISTS "Anyone can view likes" ON property_likes;
+DROP POLICY IF EXISTS "Users can create their own likes" ON property_likes;
+DROP POLICY IF EXISTS "Users can delete their own likes" ON property_likes;
+
+-- ============================================================
+-- 2. Drop old indexes (before renaming tables)
+-- ============================================================
+
+DROP INDEX IF EXISTS idx_properties_owner_id;
+DROP INDEX IF EXISTS idx_properties_address_id;
+DROP INDEX IF EXISTS idx_properties_is_private;
+DROP INDEX IF EXISTS idx_properties_views;
+DROP INDEX IF EXISTS idx_properties_likes;
+DROP INDEX IF EXISTS idx_properties_addresses_city_state;
+DROP INDEX IF EXISTS idx_properties_models_property_id;
+DROP INDEX IF EXISTS idx_properties_models_version;
+DROP INDEX IF EXISTS idx_property_likes_property_id;
+DROP INDEX IF EXISTS idx_property_likes_user_id;
+DROP INDEX IF EXISTS property_address_idx;
+DROP INDEX IF EXISTS property_owner_idx;
+DROP INDEX IF EXISTS property_is_private_idx;
+DROP INDEX IF EXISTS property_views_idx;
+DROP INDEX IF EXISTS property_likes_idx;
+
+-- ============================================================
+-- 3. Drop old triggers (before renaming tables)
+-- ============================================================
+
+DROP TRIGGER IF EXISTS update_properties_updated_at ON properties;
+DROP TRIGGER IF EXISTS update_properties_addresses_updated_at ON properties_addresses;
+DROP TRIGGER IF EXISTS update_properties_models_updated_at ON properties_models;
+
+-- ============================================================
+-- 4. Rename tables
+-- ============================================================
+
+ALTER TABLE properties_addresses RENAME TO projects_addresses;
+ALTER TABLE properties_models RENAME TO projects_models;
+ALTER TABLE property_likes RENAME TO projects_likes;
+ALTER TABLE properties RENAME TO projects;
+
+-- ============================================================
+-- 5. Rename columns (property_id β project_id)
+-- ============================================================
+
+ALTER TABLE projects_models RENAME COLUMN property_id TO project_id;
+ALTER TABLE projects_likes RENAME COLUMN property_id TO project_id;
+
+-- Rename active_property_id in auth_sessions
+ALTER TABLE auth_sessions RENAME COLUMN active_property_id TO active_project_id;
+
+-- ============================================================
+-- 6. Rename constraints
+-- ============================================================
+
+-- Rename the unique constraint on projects_likes
+ALTER TABLE projects_likes RENAME CONSTRAINT "property_likes_pkey" TO "projects_likes_pkey";
+ALTER TABLE projects_likes RENAME CONSTRAINT "property_likes_property_id_fkey" TO "projects_likes_project_id_fkey";
+ALTER TABLE projects_likes RENAME CONSTRAINT "property_likes_property_id_user_id_key" TO "projects_likes_project_id_user_id_key";
+
+-- ============================================================
+-- 7. Recreate indexes with new names
+-- ============================================================
+
+CREATE INDEX project_address_idx ON projects(address_id);
+CREATE INDEX project_owner_idx ON projects(owner_id);
+CREATE INDEX project_is_private_idx ON projects(is_private) WHERE is_private = false;
+CREATE INDEX project_views_idx ON projects(views DESC);
+CREATE INDEX project_likes_idx ON projects(likes DESC);
+CREATE INDEX idx_projects_addresses_city_state ON projects_addresses(city, state);
+CREATE INDEX idx_projects_models_project_id ON projects_models(project_id);
+CREATE INDEX idx_projects_models_version ON projects_models(project_id, version DESC);
+CREATE INDEX idx_projects_likes_project_id ON projects_likes(project_id);
+CREATE INDEX idx_projects_likes_user_id ON projects_likes(user_id);
+
+-- ============================================================
+-- 8. Recreate triggers on renamed tables
+-- ============================================================
+
+CREATE TRIGGER update_projects_updated_at
+ BEFORE UPDATE ON projects
+ FOR EACH ROW
+ EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_projects_addresses_updated_at
+ BEFORE UPDATE ON projects_addresses
+ FOR EACH ROW
+ EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_projects_models_updated_at
+ BEFORE UPDATE ON projects_models
+ FOR EACH ROW
+ EXECUTE FUNCTION update_updated_at_column();
+
+-- ============================================================
+-- 9. Recreate RLS policies with new names and table references
+-- ============================================================
+
+-- projects policies
+CREATE POLICY "Users can view own or public projects"
+ ON projects FOR SELECT
+ USING (
+ owner_id = current_setting('app.user_id', true)::TEXT
+ OR is_private = false
+ OR owner_id IS NULL
+ );
+
+CREATE POLICY "Users can insert their own projects"
+ ON projects FOR INSERT
+ WITH CHECK (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
+
+CREATE POLICY "Users can update their own projects"
+ ON projects FOR UPDATE
+ USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
+
+CREATE POLICY "Users can delete their own projects"
+ ON projects FOR DELETE
+ USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
+
+-- projects_addresses policies
+CREATE POLICY "Authenticated users can view all addresses"
+ ON projects_addresses FOR SELECT
+ USING (true);
+
+CREATE POLICY "Authenticated users can insert addresses"
+ ON projects_addresses FOR INSERT
+ WITH CHECK (true);
+
+CREATE POLICY "Authenticated users can update addresses"
+ ON projects_addresses FOR UPDATE
+ USING (true);
+
+CREATE POLICY "Authenticated users can delete addresses"
+ ON projects_addresses FOR DELETE
+ USING (true);
+
+-- projects_models policies
+CREATE POLICY "Users can view models of own or public projects"
+ ON projects_models FOR SELECT
+ USING (
+ EXISTS (
+ SELECT 1 FROM projects
+ WHERE projects.id = projects_models.project_id
+ AND (
+ projects.owner_id = current_setting('app.user_id', true)::TEXT
+ OR projects.is_private = false
+ )
+ )
+ );
+
+CREATE POLICY "Users can insert models for their own projects"
+ ON projects_models FOR INSERT
+ WITH CHECK (
+ EXISTS (
+ SELECT 1 FROM projects
+ WHERE projects.id = projects_models.project_id
+ AND projects.owner_id = current_setting('app.user_id', true)::TEXT
+ )
+ );
+
+CREATE POLICY "Users can update models of their own projects"
+ ON projects_models FOR UPDATE
+ USING (
+ EXISTS (
+ SELECT 1 FROM projects
+ WHERE projects.id = projects_models.project_id
+ AND projects.owner_id = current_setting('app.user_id', true)::TEXT
+ )
+ );
+
+CREATE POLICY "Users can delete models of their own projects"
+ ON projects_models FOR DELETE
+ USING (
+ EXISTS (
+ SELECT 1 FROM projects
+ WHERE projects.id = projects_models.project_id
+ AND projects.owner_id = current_setting('app.user_id', true)::TEXT
+ )
+ );
+
+-- projects_likes policies
+CREATE POLICY "Anyone can view likes"
+ ON projects_likes FOR SELECT
+ USING (true);
+
+CREATE POLICY "Users can create their own likes"
+ ON projects_likes FOR INSERT
+ WITH CHECK (user_id = current_setting('app.user_id', true)::TEXT);
+
+CREATE POLICY "Users can delete their own likes"
+ ON projects_likes FOR DELETE
+ USING (user_id = current_setting('app.user_id', true)::TEXT);
+
+-- ============================================================
+-- 10. Replace functions with renamed versions
+-- ============================================================
+
+-- Drop old functions
+DROP FUNCTION IF EXISTS increment_property_views(TEXT);
+DROP FUNCTION IF EXISTS get_property_like_count(TEXT);
+
+-- Recreate with new names
+CREATE OR REPLACE FUNCTION increment_project_views(project_id TEXT)
+RETURNS void AS $$
+BEGIN
+ UPDATE projects
+ SET views = views + 1
+ WHERE id = project_id;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+CREATE OR REPLACE FUNCTION get_project_like_count(project_id TEXT)
+RETURNS INTEGER AS $$
+BEGIN
+ RETURN (SELECT COUNT(*)::INTEGER FROM projects_likes WHERE projects_likes.project_id = $1);
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER STABLE;
+
+COMMIT;