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
@@ -3,9 +3,11 @@ import { useViewer } from "@pascal-app/viewer";
import { Camera, Download, Save, Trash2, Upload } from "lucide-react";
import { useRef, useState } from "react";
import { Button } from "@/components/ui/primitives/button";
import { Switch } from "@/components/ui/primitives/switch";
import useEditor from "@/store/use-editor";
import { AudioSettingsDialog } from "./audio-settings-dialog";
import { useProjectStore } from "@/features/community/lib/projects/store";
import { updateProjectVisibility } from "@/features/community/lib/projects/actions";
export function SettingsPanel() {
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -77,8 +79,74 @@ export function SettingsPanel() {
setTimeout(() => setIsGeneratingThumbnail(false), 3000);
};
const handleVisibilityChange = async (
field: 'isPrivate' | 'showScansPublic' | 'showGuidesPublic',
value: boolean,
) => {
if (!projectId) return;
// Optimistic update
useProjectStore.setState((state) => ({
activeProject: state.activeProject
? {
...state.activeProject,
...(field === 'isPrivate' && { is_private: value }),
...(field === 'showScansPublic' && { show_scans_public: value }),
...(field === 'showGuidesPublic' && { show_guides_public: value }),
}
: null,
}));
await updateProjectVisibility(projectId, { [field]: value });
};
return (
<div className="flex flex-col gap-6 p-3">
{/* Visibility Section (only for cloud projects) */}
{projectId && !isLocalProject && (
<div className="space-y-3">
<label className="font-medium text-muted-foreground text-xs uppercase">
Visibility
</label>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">Public</div>
<div className="text-xs text-muted-foreground">
{activeProject?.is_private ? 'Only you' : 'Anyone'} can view
</div>
</div>
<Switch
checked={!activeProject?.is_private}
onCheckedChange={(checked) => handleVisibilityChange('isPrivate', !checked)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">Show 3D Scans</div>
<div className="text-xs text-muted-foreground">
Visible to public viewers
</div>
</div>
<Switch
checked={activeProject?.show_scans_public ?? true}
onCheckedChange={(checked) => handleVisibilityChange('showScansPublic', checked)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">Show Floorplans</div>
<div className="text-xs text-muted-foreground">
Visible to public viewers
</div>
</div>
<Switch
checked={activeProject?.show_guides_public ?? true}
onCheckedChange={(checked) => handleVisibilityChange('showGuidesPublic', checked)}
/>
</div>
</div>
)}
{/* Export Section */}
<div className="space-y-2">
<label className="font-medium text-muted-foreground text-xs uppercase">