feat: add project visibility settings for scans and floorplans (#106)

- Add show_scans_public and show_guides_public columns to projects table
- Add visibility toggles to editor sidebar settings panel (auto-save)
- Add visibility toggles to project settings dialog (auto-save)
- Propagate settings to community viewer (hide scans/guides for non-owners)
- Add updateProjectVisibility server action
- Fix username onboarding dialog stuck after DB reset (validate update affected rows)
- Add error handling to UsernameGate for stale sessions

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aymeric Rabot
2026-02-19 16:59:23 -05:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 659c01052e
commit f4f9f8a220
12 changed files with 1253 additions and 68 deletions
+22 -4
View File
@@ -1,7 +1,7 @@
'use client'
import { initSpatialGridSync, useScene } from '@pascal-app/core'
import { Viewer } from '@pascal-app/viewer'
import { Viewer, useViewer } from '@pascal-app/viewer'
import { useParams } from 'next/navigation'
import { useEffect, useState } from 'react'
import { ViewerCameraControls } from './viewer-camera-controls'
@@ -20,6 +20,8 @@ export default function ViewerPage() {
const [projectId, setProjectId] = useState<string | null>(null)
const [projectName, setProjectName] = useState<string | null>(null)
const [owner, setOwner] = useState<ProjectOwner | null>(null)
const [canShowScans, setCanShowScans] = useState(true)
const [canShowGuides, setCanShowGuides] = useState(true)
const setScene = useScene((state) => state.setScene)
useEffect(() => {
@@ -42,10 +44,26 @@ export default function ViewerPage() {
const result = await getProjectModelPublic(id)
if (result.success && result.data) {
const { project, model } = result.data
const { project, model, isOwner } = result.data
const projectData = project as any
setProjectId(project.id)
setProjectName(project.name)
setOwner((project as any).owner ?? null)
setOwner(projectData.owner ?? null)
// Apply public visibility settings for scans/guides (only for non-owners)
if (!isOwner) {
const scansAllowed = projectData.show_scans_public !== false
const guidesAllowed = projectData.show_guides_public !== false
setCanShowScans(scansAllowed)
setCanShowGuides(guidesAllowed)
if (!scansAllowed) {
useViewer.getState().setShowScans(false)
}
if (!guidesAllowed) {
useViewer.getState().setShowGuides(false)
}
}
if (model?.scene_graph) {
const { nodes, rootNodeIds } = model.scene_graph
@@ -88,7 +106,7 @@ export default function ViewerPage() {
return (
<div className="relative h-screen w-full">
<ViewerOverlay projectName={projectName} owner={owner} />
<ViewerOverlay projectName={projectName} owner={owner} canShowScans={canShowScans} canShowGuides={canShowGuides} />
<ViewerGuestCTA />
<Viewer>
<ViewerCameraControls />
@@ -19,9 +19,11 @@ const getNodeName = (node: AnyNode): string => {
interface ViewerOverlayProps {
projectName?: string | null
owner?: ProjectOwner | null
canShowScans?: boolean
canShowGuides?: boolean
}
export const ViewerOverlay = ({ projectName, owner }: ViewerOverlayProps) => {
export const ViewerOverlay = ({ projectName, owner, canShowScans = true, canShowGuides = true }: ViewerOverlayProps) => {
const selection = useViewer((s) => s.selection)
const nodes = useScene((s) => s.nodes)
const showScans = useViewer((s) => s.showScans)
@@ -171,8 +173,10 @@ export const ViewerOverlay = ({ projectName, owner }: ViewerOverlayProps) => {
{/* Controls Panel - Top Right */}
<div className="absolute top-4 right-4 z-10 flex flex-col gap-2">
{/* Visibility Controls */}
{(canShowScans || canShowGuides) && (
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)]">
<span className="text-xs text-neutral-500 px-2 pb-1">Visibility</span>
{canShowScans && (
<button
onClick={() => useViewer.getState().setShowScans(!showScans)}
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
@@ -182,6 +186,8 @@ export const ViewerOverlay = ({ projectName, owner }: ViewerOverlayProps) => {
<Box className="w-4 h-4" />
3D Scans
</button>
)}
{canShowGuides && (
<button
onClick={() => useViewer.getState().setShowGuides(!showGuides)}
className={`flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors ${
@@ -191,7 +197,9 @@ export const ViewerOverlay = ({ projectName, owner }: ViewerOverlayProps) => {
<Image className="w-4 h-4" />
Guides
</button>
)}
</div>
)}
{/* Camera Mode */}
<div className="flex flex-col gap-1 bg-white/80 backdrop-blur-sm rounded-lg p-2 shadow-[0_1px_4px_rgba(0,0,0,0.06),0_0_0_1px_rgba(0,0,0,0.03)]">