removed non logged in editor

This commit is contained in:
wass08
2026-02-26 12:04:12 +09:00
parent 48d84e5cd9
commit 8345e42119
11 changed files with 75 additions and 606 deletions
+15 -6
View File
@@ -1,24 +1,33 @@
'use client' 'use client'
import Editor from '@/components/editor' import Editor from '@/components/editor'
import { useParams } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { useEffect, useLayoutEffect } from 'react' import { useLayoutEffect } from 'react'
import { useProjectStore } from '@/features/community/lib/projects/store' import { useProjectStore } from '@/features/community/lib/projects/store'
import { useAuth } from '@/features/community/lib/auth/hooks' import { useAuth } from '@/features/community/lib/auth/hooks'
export default function EditorPage() { export default function EditorPage() {
const params = useParams() const params = useParams()
const projectId = params.projectId as string const projectId = params.projectId as string
const { isAuthenticated } = useAuth() const { isAuthenticated, isLoading } = useAuth()
const setActiveProject = useProjectStore((state) => state.setActiveProject) const setActiveProject = useProjectStore((state) => state.setActiveProject)
const router = useRouter()
// Use layoutEffect to set active project BEFORE the editor renders and hooks run // Use layoutEffect to set active project BEFORE the editor renders and hooks run
useLayoutEffect(() => { useLayoutEffect(() => {
// For authenticated users with cloud projects, set the active project from URL if (isLoading) return
if (isAuthenticated && projectId && !projectId.startsWith('local_')) { if (!isAuthenticated) {
router.replace('/')
return
}
if (projectId) {
setActiveProject(projectId) setActiveProject(projectId)
} }
}, [projectId, isAuthenticated, setActiveProject]) }, [projectId, isAuthenticated, isLoading, setActiveProject, router])
if (isLoading || !isAuthenticated) {
return null
}
return ( return (
<div className="flex h-screen w-full max-w-screen"> <div className="flex h-screen w-full max-w-screen">
-13
View File
@@ -2,8 +2,6 @@
import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core' import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-app/core'
import { Viewer } from '@pascal-app/viewer' import { Viewer } from '@pascal-app/viewer'
import { useAuth } from '@/features/community/lib/auth/hooks'
import { useLocalProjectScene } from '@/features/community/lib/local-storage/hooks'
import { useProjectScene } from '@/features/community/lib/models/hooks' import { useProjectScene } from '@/features/community/lib/models/hooks'
import { useKeyboard } from '@/hooks/use-keyboard' import { useKeyboard } from '@/hooks/use-keyboard'
import { initSFXBus } from '@/lib/sfx-bus' import { initSFXBus } from '@/lib/sfx-bus'
@@ -38,18 +36,7 @@ interface EditorProps {
export default function Editor({ projectId }: EditorProps) { export default function Editor({ projectId }: EditorProps) {
useKeyboard() useKeyboard()
const { isAuthenticated } = useAuth()
// Determine which mode to use
const isLocalProject = projectId?.startsWith('local_')
const shouldUseCloud = isAuthenticated && !isLocalProject
const shouldUseLocal = !shouldUseCloud && !!projectId
// Call hooks unconditionally (hooks internally check if they should activate)
// Cloud hook activates when there's an activeProject in the store
useProjectScene() useProjectScene()
// Local hook activates when projectId is provided and starts with 'local_'
useLocalProjectScene(shouldUseLocal ? projectId : undefined)
return ( return (
<div className="w-full h-full"> <div className="w-full h-full">
@@ -53,8 +53,8 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial
e.target.value = '' e.target.value = ''
const projectId = activeProject?.id const projectId = activeProject?.id
if (!projectId || projectId.startsWith('local_')) { if (!projectId) {
setUploadError('Save your project to the cloud first to add references.') setUploadError('No active project. Please open a project first.')
return return
} }
@@ -93,8 +93,8 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial
e.target.value = '' e.target.value = ''
const projectId = activeProject?.id const projectId = activeProject?.id
if (!projectId || projectId.startsWith('local_')) { if (!projectId) {
setUploadError('Save your project to the cloud first to add references.') setUploadError('No active project. Please open a project first.')
return return
} }
@@ -142,7 +142,6 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial
// Delete storage asset first (before removing from scene) // Delete storage asset first (before removing from scene)
if ( if (
projectId && projectId &&
!projectId.startsWith('local_') &&
refNode?.url && refNode?.url &&
(refNode.url.startsWith('http://') || refNode.url.startsWith('https://')) (refNode.url.startsWith('http://') || refNode.url.startsWith('https://'))
) { ) {
@@ -1,32 +1,23 @@
'use client' 'use client'
import { Cloud, Home } from 'lucide-react' import { Home } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useAuth } from '../lib/auth/hooks' import { useAuth } from '../lib/auth/hooks'
import { useProjectStore } from '../lib/projects/store' import { useProjectStore } from '../lib/projects/store'
import { ProfileDropdown } from './profile-dropdown' import { ProfileDropdown } from './profile-dropdown'
import { SignInDialog } from './sign-in-dialog'
interface CloudSaveButtonProps {
projectId?: string
}
/** /**
* CloudSaveButton - Shows authentication state and project management * CloudSaveButton - Shows authentication state and project management
* *
* Guest with local project: Shows "Save to cloud" button * Guest: Shows "Home" button
* Guest without project: Shows "Home" button
* Authenticated: Shows ProfileDropdown * Authenticated: Shows ProfileDropdown
*/ */
export function CloudSaveButton({ projectId }: CloudSaveButtonProps) { export function CloudSaveButton() {
const { isAuthenticated, isLoading } = useAuth() const { isAuthenticated, isLoading } = useAuth()
const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false)
const initialize = useProjectStore(state => state.initialize) const initialize = useProjectStore(state => state.initialize)
const router = useRouter() const router = useRouter()
const isLocalProject = projectId?.startsWith('local_')
// Initialize project store when authenticated // Initialize project store when authenticated
useEffect(() => { useEffect(() => {
if (isAuthenticated) { if (isAuthenticated) {
@@ -44,25 +35,6 @@ export function CloudSaveButton({ projectId }: CloudSaveButtonProps) {
) )
} }
// Guest user with local project
if (!isAuthenticated && isLocalProject) {
return (
<>
<div className="pointer-events-auto">
<button
className="flex items-center gap-2 rounded-lg border border-border bg-background/95 px-3 py-2 text-sm font-medium shadow-lg backdrop-blur-md transition-colors hover:bg-accent hover:text-accent-foreground"
onClick={() => setIsSignInDialogOpen(true)}
>
<Cloud className="h-4 w-4" />
Save to cloud
</button>
</div>
<SignInDialog open={isSignInDialogOpen} onOpenChange={setIsSignInDialogOpen} />
</>
)
}
// Guest user (no project context or browsing)
if (!isAuthenticated) { if (!isAuthenticated) {
return ( return (
<div className="pointer-events-auto"> <div className="pointer-events-auto">
@@ -77,7 +49,6 @@ export function CloudSaveButton({ projectId }: CloudSaveButtonProps) {
) )
} }
// Authenticated user
return ( return (
<div className="pointer-events-auto"> <div className="pointer-events-auto">
<ProfileDropdown /> <ProfileDropdown />
@@ -4,8 +4,6 @@ import Image from 'next/image'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useAuth } from '../lib/auth/hooks' import { useAuth } from '../lib/auth/hooks'
import type { LocalProject } from '../lib/local-storage/project-store'
import { createLocalProject, getLocalProjects } from '../lib/local-storage/project-store'
import { getPublicProjects, getUserProjects } from '../lib/projects/actions' import { getPublicProjects, getUserProjects } from '../lib/projects/actions'
import type { Project } from '../lib/projects/types' import type { Project } from '../lib/projects/types'
import { CreateProjectButton } from './create-project-button' import { CreateProjectButton } from './create-project-button'
@@ -16,27 +14,23 @@ import { ProjectGrid } from './project-grid'
import { SignInDialog } from './sign-in-dialog' import { SignInDialog } from './sign-in-dialog'
export default function CommunityHub() { export default function CommunityHub() {
const { isAuthenticated, isLoading: authLoading, user } = useAuth() const { isAuthenticated, isLoading: authLoading } = useAuth()
const router = useRouter() const router = useRouter()
const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false) const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false)
const [isNewProjectDialogOpen, setIsNewProjectDialogOpen] = useState(false) const [isNewProjectDialogOpen, setIsNewProjectDialogOpen] = useState(false)
const [localProjectToSave, setLocalProjectToSave] = useState<LocalProject | null>(null)
const [publicProjects, setPublicProjects] = useState<Project[]>([]) const [publicProjects, setPublicProjects] = useState<Project[]>([])
const [userProjects, setUserProjects] = useState<Project[]>([]) const [userProjects, setUserProjects] = useState<Project[]>([])
const [localProjects, setLocalProjects] = useState<LocalProject[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
useEffect(() => { useEffect(() => {
async function loadProjects() { async function loadProjects() {
setLoading(true) setLoading(true)
// Load public projects (always)
const publicResult = await getPublicProjects() const publicResult = await getPublicProjects()
if (publicResult.success) { if (publicResult.success) {
setPublicProjects(publicResult.data || []) setPublicProjects(publicResult.data || [])
} }
// Load user projects if authenticated
if (isAuthenticated) { if (isAuthenticated) {
const userResult = await getUserProjects() const userResult = await getUserProjects()
if (userResult.success) { if (userResult.success) {
@@ -44,9 +38,6 @@ export default function CommunityHub() {
} }
} }
// Always load local projects
setLocalProjects(getLocalProjects())
setLoading(false) setLoading(false)
} }
@@ -55,27 +46,7 @@ export default function CommunityHub() {
} }
}, [isAuthenticated, authLoading]) }, [isAuthenticated, authLoading])
const handleCreateProject = async () => {
if (!isAuthenticated) {
// Create local project for guest
const project = createLocalProject('Untitled Project')
router.push(`/editor/${project.id}`)
} else {
// Open project creation dialog for authenticated users
setIsNewProjectDialogOpen(true)
}
}
const handleProjectCreated = async (projectId: string) => { const handleProjectCreated = async (projectId: string) => {
// If this was a local project being saved, delete it from localStorage
if (localProjectToSave) {
const { deleteLocalProject } = await import('../lib/local-storage/project-store')
deleteLocalProject(localProjectToSave.id)
setLocalProjects(getLocalProjects())
setLocalProjectToSave(null)
}
// Reload projects and navigate to the new project
const result = await getUserProjects() const result = await getUserProjects()
if (result.success) { if (result.success) {
setUserProjects(result.data || []) setUserProjects(result.data || [])
@@ -83,11 +54,6 @@ export default function CommunityHub() {
router.push(`/editor/${projectId}`) router.push(`/editor/${projectId}`)
} }
const handleSaveLocalToCloud = (localProject: LocalProject) => {
setLocalProjectToSave(localProject)
setIsNewProjectDialogOpen(true)
}
const handleProjectClick = (projectId: string) => { const handleProjectClick = (projectId: string) => {
router.push(`/editor/${projectId}`) router.push(`/editor/${projectId}`)
} }
@@ -153,18 +119,17 @@ export default function CommunityHub() {
<section> <section>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold">My Projects</h2> <h2 className="text-xl font-semibold">My Projects</h2>
<CreateProjectButton onCreateProject={handleCreateProject} /> <CreateProjectButton onCreateProject={() => setIsNewProjectDialogOpen(true)} />
</div> </div>
{userProjects.length === 0 && localProjects.length === 0 ? ( {userProjects.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center"> <div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-border py-16 text-center">
<p className="text-muted-foreground">You don&apos;t have any projects yet.</p> <p className="text-muted-foreground">You don&apos;t have any projects yet.</p>
</div> </div>
) : ( ) : (
<ProjectGrid <ProjectGrid
projects={[...userProjects, ...localProjects]} projects={userProjects}
onProjectClick={handleProjectClick} onProjectClick={handleProjectClick}
onViewClick={handleViewProject} onViewClick={handleViewProject}
onSaveToCloud={handleSaveLocalToCloud}
showOwner={false} showOwner={false}
canEdit canEdit
onUpdate={() => { onUpdate={() => {
@@ -181,30 +146,19 @@ export default function CommunityHub() {
</section> </section>
)} )}
{/* Local Projects Section (Guest Users) */} {/* Sign-in CTA for unauthenticated users */}
{!isAuthenticated && localProjects.length > 0 && ( {!isAuthenticated && (
<section> <section className="rounded-2xl border border-border bg-neutral-50 dark:bg-neutral-900/50 px-8 py-12 text-center">
<div className="flex items-center justify-between mb-6"> <h2 className="text-2xl font-semibold mb-2">Build with Pascal</h2>
<h2 className="text-xl font-semibold">My Local Projects</h2> <p className="text-muted-foreground mb-6 max-w-md mx-auto">
<CreateProjectButton onCreateProject={handleCreateProject} /> Create and share 3D architectural projects. Sign in to get started.
</div>
<ProjectGrid
projects={localProjects}
onProjectClick={handleProjectClick}
showOwner={false}
isLocal
/>
</section>
)}
{/* Create First Project CTA */}
{!isAuthenticated && localProjects.length === 0 && (
<section className="text-center py-12">
<h2 className="text-2xl font-semibold mb-4">Get Started</h2>
<p className="text-muted-foreground mb-6">
Create your first project to start designing
</p> </p>
<CreateProjectButton onCreateProject={handleCreateProject} /> <button
onClick={() => setIsSignInDialogOpen(true)}
className="rounded-lg bg-primary px-6 py-2.5 text-primary-foreground font-medium hover:bg-primary/90 transition-colors"
>
Sign in to create a project
</button>
</section> </section>
)} )}
@@ -230,15 +184,6 @@ export default function CommunityHub() {
open={isNewProjectDialogOpen} open={isNewProjectDialogOpen}
onOpenChange={setIsNewProjectDialogOpen} onOpenChange={setIsNewProjectDialogOpen}
onSuccess={handleProjectCreated} onSuccess={handleProjectCreated}
localProjectData={
localProjectToSave
? {
id: localProjectToSave.id,
name: localProjectToSave.name,
sceneGraph: localProjectToSave.scene_graph,
}
: undefined
}
/> />
</div> </div>
) )
@@ -1,83 +0,0 @@
'use client'
import { useState } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/primitives/dialog'
import type { LocalProject } from '../lib/local-storage/project-store'
interface LocalProjectMigrationDialogProps {
localProjects: LocalProject[]
open: boolean
onMigrate: () => Promise<void>
onSkip: () => void
}
export function LocalProjectMigrationDialog({
localProjects,
open,
onMigrate,
onSkip,
}: LocalProjectMigrationDialogProps) {
const [isMigrating, setIsMigrating] = useState(false)
const handleMigrate = async () => {
setIsMigrating(true)
try {
await onMigrate()
} finally {
setIsMigrating(false)
}
}
return (
<Dialog open={open} onOpenChange={(open) => !open && !isMigrating && onSkip()}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Save Local Projects to Cloud</DialogTitle>
<DialogDescription>
You have {localProjects.length} local {localProjects.length === 1 ? 'project' : 'projects'} that {localProjects.length === 1 ? 'hasn\'t' : 'haven\'t'} been saved to the cloud yet.
</DialogDescription>
</DialogHeader>
<div className="space-y-2 py-4">
<p className="text-sm text-muted-foreground">
Would you like to save {localProjects.length === 1 ? 'it' : 'them'} to your account?
</p>
<ul className="space-y-1 text-sm">
{localProjects.map((project) => (
<li key={project.id} className="flex items-center gap-2">
<span className="text-muted-foreground"></span>
<span className="font-medium">{project.name}</span>
</li>
))}
</ul>
</div>
<DialogFooter>
<button
type="button"
onClick={onSkip}
className="rounded-md border border-border px-4 py-2 text-sm hover:bg-accent"
disabled={isMigrating}
>
Skip for now
</button>
<button
type="button"
onClick={handleMigrate}
className="rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground hover:bg-primary/90"
disabled={isMigrating}
>
{isMigrating ? 'Saving...' : 'Save to Cloud'}
</button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -2,26 +2,21 @@
import { X } from 'lucide-react' import { X } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { createProject } from '../lib/projects/actions'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/primitives/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/primitives/dialog'
import { Switch } from '@/components/ui/primitives/switch' import { Switch } from '@/components/ui/primitives/switch'
import { createProject } from '../lib/projects/actions'
interface NewProjectDialogProps { interface NewProjectDialogProps {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
onSuccess?: (projectId: string) => void onSuccess?: (projectId: string) => void
localProjectData?: {
id: string
name: string
sceneGraph: any
}
} }
/** /**
* NewProjectDialog - Dialog for creating a new project with optional Google Maps address search * NewProjectDialog - Dialog for creating a new project
*/ */
export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectData }: NewProjectDialogProps) { export function NewProjectDialog({ open, onOpenChange, onSuccess }: NewProjectDialogProps) {
const [projectName, setProjectName] = useState(localProjectData?.name || '') const [projectName, setProjectName] = useState('')
const [isPrivate, setIsPrivate] = useState(false) const [isPrivate, setIsPrivate] = useState(false)
const [isCreating, setIsCreating] = useState(false) const [isCreating, setIsCreating] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -32,19 +27,10 @@ export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectDa
const name = projectName.trim() || 'Untitled Project' const name = projectName.trim() || 'Untitled Project'
if (!name) {
setError('Please enter a project name')
return
}
setIsCreating(true) setIsCreating(true)
try { try {
const result = await createProject({ const result = await createProject({ name, isPrivate })
name,
isPrivate,
sceneGraph: localProjectData?.sceneGraph,
})
if (result.success && result.data) { if (result.success && result.data) {
onOpenChange(false) onOpenChange(false)
@@ -73,7 +59,7 @@ export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectDa
return ( return (
<Dialog open={open} onOpenChange={handleClose} modal={false}> <Dialog open={open} onOpenChange={handleClose} modal={false}>
<DialogContent <DialogContent
className="sm:max-w-[500px]" className="sm:max-w-125"
onInteractOutside={(e) => e.preventDefault()} onInteractOutside={(e) => e.preventDefault()}
> >
<DialogHeader> <DialogHeader>
@@ -120,17 +106,6 @@ export function NewProjectDialog({ open, onOpenChange, onSuccess, localProjectDa
</div> </div>
</div> </div>
{localProjectData && (
<div className="rounded-md border border-blue-500/50 bg-blue-500/10 p-3 text-sm">
<p className="font-medium text-blue-700 dark:text-blue-300">
Saving local project: {localProjectData.name}
</p>
<p className="mt-1 text-xs text-muted-foreground">
Your building data will be preserved
</p>
</div>
)}
{error && ( {error && (
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm"> <div className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm">
{error} {error}
@@ -3,34 +3,25 @@
import { Eye, Heart, Settings } from 'lucide-react' import { Eye, Heart, Settings } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import type { Project } from '../lib/projects/types'
import type { LocalProject } from '../lib/local-storage/project-store'
import { ProjectSettingsDialog } from './project-settings-dialog'
import { getUserProjectLikes, toggleProjectLike } from '../lib/projects/actions'
import { useAuth } from '../lib/auth/hooks' import { useAuth } from '../lib/auth/hooks'
import { getUserProjectLikes, toggleProjectLike } from '../lib/projects/actions'
import type { Project } from '../lib/projects/types'
import { ProjectSettingsDialog } from './project-settings-dialog'
interface ProjectGridProps { interface ProjectGridProps {
projects: (Project | LocalProject)[] projects: Project[]
onProjectClick: (id: string) => void onProjectClick: (id: string) => void
onViewClick?: (id: string) => void onViewClick?: (id: string) => void
onSaveToCloud?: (project: LocalProject) => void
showOwner: boolean showOwner: boolean
isLocal?: boolean
canEdit?: boolean canEdit?: boolean
onUpdate?: () => void onUpdate?: () => void
} }
function isLocalProject(prop: Project | LocalProject): prop is LocalProject {
return 'is_local' in prop && prop.is_local === true
}
export function ProjectGrid({ export function ProjectGrid({
projects, projects,
onProjectClick, onProjectClick,
onViewClick, onViewClick,
onSaveToCloud,
showOwner, showOwner,
isLocal = false,
canEdit = false, canEdit = false,
onUpdate, onUpdate,
}: ProjectGridProps) { }: ProjectGridProps) {
@@ -39,28 +30,21 @@ export function ProjectGrid({
const [userLikes, setUserLikes] = useState<Record<string, boolean>>({}) const [userLikes, setUserLikes] = useState<Record<string, boolean>>({})
const [likeCounts, setLikeCounts] = useState<Record<string, number>>({}) const [likeCounts, setLikeCounts] = useState<Record<string, number>>({})
// Initialize like counts from projects
useEffect(() => { useEffect(() => {
const counts: Record<string, number> = {} const counts: Record<string, number> = {}
projects.forEach((proj) => { projects.forEach((proj) => {
if (!isLocalProject(proj)) {
counts[proj.id] = proj.likes counts[proj.id] = proj.likes
}
}) })
setLikeCounts(counts) setLikeCounts(counts)
}, [projects]) }, [projects])
// Fetch which projects the user has liked
useEffect(() => { useEffect(() => {
if (!isAuthenticated) { if (!isAuthenticated) {
setUserLikes({}) setUserLikes({})
return return
} }
const projectIds = projects const projectIds = projects.map((p) => p.id)
.filter((p) => !isLocalProject(p))
.map((p) => p.id)
if (projectIds.length === 0) return if (projectIds.length === 0) return
getUserProjectLikes(projectIds).then((result) => { getUserProjectLikes(projectIds).then((result) => {
@@ -70,12 +54,10 @@ export function ProjectGrid({
}) })
}, [projects, isAuthenticated]) }, [projects, isAuthenticated])
const handleSettingsClick = (e: React.MouseEvent, project: Project | LocalProject) => { const handleSettingsClick = (e: React.MouseEvent, project: Project) => {
e.stopPropagation() e.stopPropagation()
if (!isLocalProject(project)) {
setSettingsProject(project) setSettingsProject(project)
} }
}
const handleViewClick = (e: React.MouseEvent, projectId: string) => { const handleViewClick = (e: React.MouseEvent, projectId: string) => {
e.stopPropagation() e.stopPropagation()
@@ -85,31 +67,24 @@ export function ProjectGrid({
const handleLikeClick = async (e: React.MouseEvent, projectId: string) => { const handleLikeClick = async (e: React.MouseEvent, projectId: string) => {
e.stopPropagation() e.stopPropagation()
if (!isAuthenticated) { if (!isAuthenticated) return
// Could show a sign-in prompt here
return
}
// Optimistic update
const wasLiked = userLikes[projectId] || false const wasLiked = userLikes[projectId] || false
const currentCount = likeCounts[projectId] || 0 const currentCount = likeCounts[projectId] || 0
setUserLikes((prev) => ({ ...prev, [projectId]: !wasLiked })) setUserLikes((prev) => ({ ...prev, [projectId]: !wasLiked }))
setLikeCounts((prev) => ({ setLikeCounts((prev) => ({
...prev, ...prev,
[projectId]: wasLiked ? currentCount - 1 : currentCount + 1 [projectId]: wasLiked ? currentCount - 1 : currentCount + 1,
})) }))
// Call server action
const result = await toggleProjectLike(projectId) const result = await toggleProjectLike(projectId)
if (result.success && result.data) { if (result.success && result.data) {
// Update with actual values from server
const data = result.data const data = result.data
setUserLikes((prev) => ({ ...prev, [projectId]: data.liked })) setUserLikes((prev) => ({ ...prev, [projectId]: data.liked }))
setLikeCounts((prev) => ({ ...prev, [projectId]: data.likes })) setLikeCounts((prev) => ({ ...prev, [projectId]: data.likes }))
} else { } else {
// Revert on error
setUserLikes((prev) => ({ ...prev, [projectId]: wasLiked })) setUserLikes((prev) => ({ ...prev, [projectId]: wasLiked }))
setLikeCounts((prev) => ({ ...prev, [projectId]: currentCount })) setLikeCounts((prev) => ({ ...prev, [projectId]: currentCount }))
} }
@@ -119,7 +94,7 @@ export function ProjectGrid({
<> <>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
{projects.map((project) => { {projects.map((project) => {
const owner = !isLocalProject(project) ? project.owner : null const owner = project.owner
return ( return (
<div <div
@@ -129,7 +104,7 @@ export function ProjectGrid({
> >
{/* Thumbnail card */} {/* Thumbnail card */}
<div className="relative aspect-[4/3] rounded-xl rounded-smooth-xl bg-neutral-50 overflow-hidden shadow-[0_1px_3px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.04)] transition-shadow group-hover:shadow-[0_4px_12px_rgba(0,0,0,0.08),0_0_0_1px_rgba(0,0,0,0.04)]"> <div className="relative aspect-[4/3] rounded-xl rounded-smooth-xl bg-neutral-50 overflow-hidden shadow-[0_1px_3px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.04)] transition-shadow group-hover:shadow-[0_4px_12px_rgba(0,0,0,0.08),0_0_0_1px_rgba(0,0,0,0.04)]">
{!isLocalProject(project) && project.thumbnail_url ? ( {project.thumbnail_url ? (
<img <img
src={project.thumbnail_url} src={project.thumbnail_url}
alt={project.name} alt={project.name}
@@ -140,27 +115,7 @@ export function ProjectGrid({
No preview No preview
</div> </div>
)} )}
{isLocalProject(project) && ( {canEdit && (
<div className="absolute top-3 right-3">
{isAuthenticated && onSaveToCloud ? (
<button
onClick={(e) => {
e.stopPropagation()
onSaveToCloud(project)
}}
className="bg-blue-500 hover:bg-blue-600 text-white text-xs px-2.5 py-1 rounded-md transition-colors"
title="Save to cloud"
>
Save to cloud
</button>
) : (
<div className="bg-blue-500 text-white text-xs px-2.5 py-1 rounded-md">
Local
</div>
)}
</div>
)}
{canEdit && !isLocalProject(project) && (
<div className="absolute top-3 right-3 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="absolute top-3 right-3 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{onViewClick && ( {onViewClick && (
<button <button
@@ -186,7 +141,6 @@ export function ProjectGrid({
{/* Info row below the card */} {/* Info row below the card */}
<div className="flex items-center gap-3 mt-3"> <div className="flex items-center gap-3 mt-3">
{/* Avatar */}
{showOwner && owner ? ( {showOwner && owner ? (
<Link <Link
href={owner.username ? `/u/${owner.username}` : '#'} href={owner.username ? `/u/${owner.username}` : '#'}
@@ -207,7 +161,6 @@ export function ProjectGrid({
</Link> </Link>
) : null} ) : null}
{/* Name + stats */}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="font-medium text-sm truncate">{project.name}</h3> <h3 className="font-medium text-sm truncate">{project.name}</h3>
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-0.5"> <div className="flex items-center gap-1 text-xs text-muted-foreground mt-0.5">
@@ -220,11 +173,9 @@ export function ProjectGrid({
> >
{owner.username || owner.name} {owner.username || owner.name}
</Link> </Link>
{!isLocalProject(project) && <span className="shrink-0">·</span>} <span className="shrink-0">·</span>
</> </>
)} )}
{!isLocalProject(project) && (
<>
<div className="flex items-center gap-0.5 shrink-0"> <div className="flex items-center gap-0.5 shrink-0">
<Eye className="w-3.5 h-3.5" /> <Eye className="w-3.5 h-3.5" />
<span>{project.views}</span> <span>{project.views}</span>
@@ -237,18 +188,11 @@ export function ProjectGrid({
> >
<Heart <Heart
className={`w-3.5 h-3.5 ${ className={`w-3.5 h-3.5 ${
userLikes[project.id] userLikes[project.id] ? 'fill-red-500 text-red-500' : ''
? 'fill-red-500 text-red-500'
: ''
}`} }`}
/> />
<span>{likeCounts[project.id] ?? project.likes}</span> <span>{likeCounts[project.id] ?? project.likes}</span>
</button> </button>
</>
)}
{isLocalProject(project) && (
<span>{new Date(project.updated_at).toLocaleDateString()}</span>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -257,7 +201,6 @@ export function ProjectGrid({
})} })}
</div> </div>
{/* Settings Dialog */}
{settingsProject && ( {settingsProject && (
<ProjectSettingsDialog <ProjectSettingsDialog
project={settingsProject} project={settingsProject}
@@ -1,93 +0,0 @@
'use client'
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 { getLocalProject, updateLocalProjectScene } from './project-store'
/**
* Hook for local project scene management (guest users)
* Loads scene from localStorage and auto-saves changes
*/
export function useLocalProjectScene(projectId?: string) {
const saveTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
const currentProjectIdRef = useRef<string | null>(null)
const lastProjectIdRef = useRef<string | null>(null)
// Load scene when project ID changes
useEffect(() => {
if (!projectId || !projectId.startsWith('local_')) {
return
}
if (lastProjectIdRef.current === projectId) {
return
}
lastProjectIdRef.current = projectId
currentProjectIdRef.current = projectId
const project = getLocalProject(projectId)
if (project?.scene_graph) {
const { nodes, rootNodeIds } = project.scene_graph
useScene.getState().setScene(nodes, rootNodeIds as AnyNodeId[])
initSpatialGridSync()
} else {
useScene.getState().clearScene()
}
useEditor.getState().setPhase('site')
useViewer.getState().setSelection({
buildingId: null,
levelId: null,
selectedIds: [],
zoneId: null,
})
}, [projectId])
// Auto-save to localStorage with debouncing
useEffect(() => {
if (!projectId || !projectId.startsWith('local_')) {
currentProjectIdRef.current = null
return
}
currentProjectIdRef.current = projectId
let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
const unsubscribe = useScene.subscribe((state) => {
const currentNodesSnapshot = JSON.stringify(state.nodes)
if (currentNodesSnapshot === lastNodesSnapshot) {
return
}
lastNodesSnapshot = currentNodesSnapshot
const nodes = state.nodes
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
// Debounce save by 1 second (faster than cloud save)
saveTimeoutRef.current = setTimeout(() => {
const currentId = currentProjectIdRef.current
if (!currentId) return
const rootNodeIds = useScene.getState().rootNodeIds
const sceneGraph = { nodes, rootNodeIds }
updateLocalProjectScene(currentId, sceneGraph)
}, 1000)
})
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current)
}
unsubscribe()
}
}, [projectId])
}
@@ -1,114 +0,0 @@
/**
* Local storage management for guest users
* Stores projects and scenes in browser localStorage
*/
import { createId } from '../utils/id-generator'
export interface SceneGraph {
nodes: Record<string, any>
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)
}
@@ -803,76 +803,6 @@ export async function updateProjectAddress(
} }
} }
/**
* Migrate a local project to the cloud
* Creates a new project with the local project's data
*/
export async function migrateLocalProject(
localProject: {
name: string
scene_graph: any
},
): Promise<ActionResult<{ id: string }>> {
try {
const session = await getSession()
if (!session?.user) {
return {
success: false,
error: 'Not authenticated',
}
}
const supabase = await createServerSupabaseClient()
// 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: null,
is_private: true, // Default to private
})
if (projectError) {
return {
success: false,
error: projectError.message,
}
}
// Create the model with the scene graph
if (localProject.scene_graph) {
const modelId = createId('model')
const { error: modelError } = await (supabase.from('projects_models') as any).insert({
id: modelId,
project_id: projectId,
version: 1,
scene_graph: localProject.scene_graph,
})
if (modelError) {
return {
success: false,
error: modelError.message,
}
}
}
return {
success: true,
data: { id: projectId },
message: 'Project migrated successfully',
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to migrate project',
}
}
}
/** /**
* Delete a project * Delete a project
* Only the owner can delete their project * Only the owner can delete their project