diff --git a/apps/editor/app/editor/[projectId]/page.tsx b/apps/editor/app/editor/[projectId]/page.tsx index 8fa64c75..d3b40e7e 100644 --- a/apps/editor/app/editor/[projectId]/page.tsx +++ b/apps/editor/app/editor/[projectId]/page.tsx @@ -1,11 +1,11 @@ 'use client' -import Editor from '@/components/editor' import { useParams, useRouter } from 'next/navigation' import { useEffect, useState } from 'react' -import { useProjectStore } from '@/features/community/lib/projects/store' -import { useAuth } from '@/features/community/lib/auth/hooks' +import Editor from '@/components/editor' import { SceneLoader } from '@/components/ui/scene-loader' +import { useAuth } from '@/features/community/lib/auth/hooks' +import { useProjectStore } from '@/features/community/lib/projects/store' export default function EditorPage() { const params = useParams() diff --git a/apps/editor/components/editor/grid.tsx b/apps/editor/components/editor/grid.tsx index 1b5c9c0b..d2577285 100644 --- a/apps/editor/components/editor/grid.tsx +++ b/apps/editor/components/editor/grid.tsx @@ -32,6 +32,12 @@ export const Grid = ({ fadeStrength?: number revealRadius?: number }) => { + const theme = useViewer((state) => state.theme) + + // Use slightly lighter colors for dark mode grid to make it apparent + const effectiveCellColor = theme === 'dark' ? '#555566' : cellColor + const effectiveSectionColor = theme === 'dark' ? '#666677' : sectionColor + const cursorPositionRef = useRef(new Vector2(0, 0)) const material = useMemo(() => { @@ -78,13 +84,16 @@ export const Grid = ({ // Mix colors based on section grid const gridColor = mix( - color(cellColor), - color(sectionColor), + color(effectiveCellColor), + color(effectiveSectionColor), float(sectionThickness).mul(g2).min(1), ) - // Combined alpha with cursor fade - const alpha = g1.add(g2).mul(fade).mul(cursorFade) + // Baseline alpha: small amount of opacity everywhere the grid exists + const baseAlpha = float(0.4) // Subtle global visibility + + // Combined alpha with cursor fade and baseline minimum + const alpha = g1.add(g2).mul(fade).mul(cursorFade.max(baseAlpha)) const finalAlpha = mix(alpha.mul(0.75), alpha, g2) return new MeshBasicNodeMaterial({ @@ -96,13 +105,14 @@ export const Grid = ({ }, [ cellSize, cellThickness, - cellColor, + effectiveCellColor, sectionSize, sectionThickness, - sectionColor, + effectiveSectionColor, fadeDistance, fadeStrength, revealRadius, + theme, ]) const gridRef = useRef(null!) @@ -137,8 +147,10 @@ export const Grid = ({ setGridY(newY) }) + const showGrid = useViewer((state) => state.showGrid) + return ( - + ) diff --git a/apps/editor/components/ui/sidebar/app-sidebar.tsx b/apps/editor/components/ui/sidebar/app-sidebar.tsx index 2da9d9a4..43163799 100644 --- a/apps/editor/components/ui/sidebar/app-sidebar.tsx +++ b/apps/editor/components/ui/sidebar/app-sidebar.tsx @@ -2,7 +2,8 @@ import { useState, useRef, useEffect, useCallback } from "react"; import { IconRail, type PanelId } from "./icon-rail"; -import { Pencil } from "lucide-react"; +import { Pencil, Moon, Sun, Monitor } from "lucide-react"; +import { motion } from "framer-motion"; import { Sidebar, @@ -14,15 +15,23 @@ import { SettingsPanel } from "./panels/settings-panel"; import { SitePanel } from "./panels/site-panel"; import { useProjectStore } from "@/features/community/lib/projects/store"; import { updateProjectName } from "@/features/community/lib/projects/actions"; +import { useViewer } from "@pascal-app/viewer"; export function AppSidebar() { const [activePanel, setActivePanel] = useState("site"); const activeProject = useProjectStore((s) => s.activeProject); - + const theme = useViewer((state) => state.theme); + const setTheme = useViewer((state) => state.setTheme); + const [mounted, setMounted] = useState(false); + const [isEditingTitle, setIsEditingTitle] = useState(false); const [titleValue, setTitleValue] = useState(""); const inputRef = useRef(null); + useEffect(() => { + setMounted(true); + }, []); + useEffect(() => { if (isEditingTitle) { setTitleValue(activeProject?.name || "Untitled Project"); @@ -93,29 +102,84 @@ export function AppSidebar() { {/* Panel Content */}
- - {isEditingTitle ? ( - setTitleValue(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={handleSaveTitle} - placeholder="Untitled Project" - className="w-full bg-transparent text-foreground outline-none border-b border-primary/50 focus:border-primary rounded-none px-0 py-0 m-0 h-7 font-semibold text-lg" - /> - ) : ( -
setIsEditingTitle(true)} - > -

- {activeProject?.name || "Untitled Project"} -

- + +
+
+ {isEditingTitle ? ( + setTitleValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleSaveTitle} + placeholder="Untitled Project" + className="w-full bg-transparent text-foreground outline-none border-b border-primary/50 focus:border-primary rounded-none px-0 py-0 m-0 h-7 font-semibold text-lg" + /> + ) : ( +
setIsEditingTitle(true)} + > +

+ {activeProject?.name || "Untitled Project"} +

+ +
+ )}
- )} + + {mounted && ( + + )} +
+ {getPanelTitle()} diff --git a/apps/editor/components/ui/sidebar/icon-rail.tsx b/apps/editor/components/ui/sidebar/icon-rail.tsx index 93e78e2a..0fd8a443 100644 --- a/apps/editor/components/ui/sidebar/icon-rail.tsx +++ b/apps/editor/components/ui/sidebar/icon-rail.tsx @@ -1,14 +1,16 @@ "use client"; -import { Building2, Settings } from "lucide-react"; +import { Moon, Sun } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; +import { useEffect, useState } from "react"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/primitives/tooltip"; import { cn } from "@/lib/utils"; +import { useViewer } from "@pascal-app/viewer"; export type PanelId = "site" | "settings"; @@ -18,9 +20,9 @@ interface IconRailProps { className?: string; } -const panels: { id: PanelId; icon: typeof Building2; label: string }[] = [ - { id: "site", icon: Building2, label: "Site" }, - { id: "settings", icon: Settings, label: "Settings" }, +const panels: { id: PanelId; iconSrc: string; label: string }[] = [ + { id: "site", iconSrc: "/icons/level.png", label: "Site" }, + { id: "settings", iconSrc: "/icons/settings.png", label: "Settings" }, ]; export function IconRail({ @@ -28,10 +30,18 @@ export function IconRail({ onPanelChange, className, }: IconRailProps) { + const theme = useViewer((state) => state.theme); + const setTheme = useViewer((state) => state.setTheme); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + return (
@@ -58,7 +68,6 @@ export function IconRail({
{panels.map((panel) => { - const Icon = panel.icon; const isActive = activePanel === panel.id; return ( @@ -67,19 +76,45 @@ export function IconRail({ className={cn( "flex h-9 w-9 items-center justify-center rounded-lg transition-all", isActive - ? "bg-primary text-primary-foreground" - : "text-muted-foreground hover:bg-accent hover:text-accent-foreground", + ? "bg-accent" + : "hover:bg-accent", )} onClick={() => onPanelChange(panel.id)} type="button" > - + {panel.label} {panel.label} ); })} + + {/* Spacer */} +
+ + {/* Theme Toggle */} + {mounted && ( + + + + + Toggle theme + + )}
); } diff --git a/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx index 71113dcb..f2997a7a 100644 --- a/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx @@ -309,6 +309,18 @@ export function SettingsPanel() { onCheckedChange={(checked) => handleVisibilityChange('showGuidesPublic', checked)} />
+
+
+
Show Grid
+
+ Visible only in the editor +
+
+ state.showGrid)} + onCheckedChange={(checked) => useViewer.getState().setShowGrid(checked)} + /> +
)} diff --git a/apps/editor/features/community/README.md b/apps/editor/features/community/README.md index 4de77f66..fbe028f8 100644 --- a/apps/editor/features/community/README.md +++ b/apps/editor/features/community/README.md @@ -162,7 +162,7 @@ This feature requires: - `properties` - Property records - `properties_addresses` - Property addresses - `properties_models` - Scene graph models -- Database migrations are managed in `packages/db/supabase/migrations/` +- Database migrations are managed in `supabase/migrations/` ## Development diff --git a/apps/editor/features/community/lib/models/actions.ts b/apps/editor/features/community/lib/models/actions.ts index 0aad71f4..2bdea9cb 100644 --- a/apps/editor/features/community/lib/models/actions.ts +++ b/apps/editor/features/community/lib/models/actions.ts @@ -10,6 +10,7 @@ import { createServerSupabaseClient } from '../database/server' import { getSession } from '../auth/server' import { createId } from '../utils/id-generator' import type { ActionResult } from '../projects/actions' +import { isSceneGraphEmpty } from './scene-graph-utils' export interface SceneGraph { nodes: Record @@ -166,6 +167,14 @@ export async function saveProjectModel( } } + // Determine if scene graph is empty + const isEmpty = isSceneGraphEmpty(sceneGraph) + + // Update the project's is_empty flag + await (supabase.from('projects') as any) + .update({ is_empty: isEmpty }) + .eq('id', projectId) + // Check if a model already exists const { data: existingModel } = await supabase .from('projects_models') diff --git a/apps/editor/features/community/lib/models/scene-graph-utils.ts b/apps/editor/features/community/lib/models/scene-graph-utils.ts new file mode 100644 index 00000000..af4edba8 --- /dev/null +++ b/apps/editor/features/community/lib/models/scene-graph-utils.ts @@ -0,0 +1,25 @@ +import type { SceneGraph } from './actions' + +const DEFAULT_NODE_TYPES = ['site', 'building', 'level'] + +export function isSceneGraphEmpty(sceneGraph: SceneGraph | any): boolean { + if (!sceneGraph?.nodes) return true + + const nodes = Object.values(sceneGraph.nodes) as any[] + + if (nodes.length > 3) { + return false + } + + const hasNonDefaultNodes = nodes.some((n) => !DEFAULT_NODE_TYPES.includes(n.type)) + if (hasNonDefaultNodes) { + return false + } + + const levelNode = nodes.find((n) => n.type === 'level') + if (Array.isArray(levelNode?.children) && levelNode.children.length > 0) { + return false + } + + return true +} diff --git a/apps/editor/features/community/lib/projects/actions.ts b/apps/editor/features/community/lib/projects/actions.ts index a00efd9d..ac0dd3bf 100644 --- a/apps/editor/features/community/lib/projects/actions.ts +++ b/apps/editor/features/community/lib/projects/actions.ts @@ -8,7 +8,8 @@ import { createServerSupabaseClient } from '../database/server' import { getSession } from '../auth/server' import { createId } from '../utils/id-generator' -import type { CreateProjectParams, Project, Database } from './types' +import { isSceneGraphEmpty } from '../models/scene-graph-utils' +import type { CreateProjectParams, Project } from './types' export type ActionResult = { success: boolean @@ -250,6 +251,9 @@ export async function createProject(params: CreateProjectParams): Promise> { owner:auth_users!owner_id(id, name, username, image) `) .eq('is_private', false) + .eq('is_empty', false) .order('views', { ascending: false }) .limit(50) diff --git a/apps/editor/features/community/lib/projects/types.ts b/apps/editor/features/community/lib/projects/types.ts index c286b77d..b8b5f40e 100644 --- a/apps/editor/features/community/lib/projects/types.ts +++ b/apps/editor/features/community/lib/projects/types.ts @@ -13,6 +13,7 @@ export type DbProject = { created_at: string updated_at: string is_private: boolean + is_empty: boolean show_scans_public: boolean show_guides_public: boolean views: number @@ -106,6 +107,7 @@ export type Project = { updated_at: string // Community features is_private: boolean + is_empty: boolean show_scans_public: boolean show_guides_public: boolean views: number diff --git a/apps/editor/public/icons/settings.png b/apps/editor/public/icons/settings.png new file mode 100644 index 00000000..14cb0c68 Binary files /dev/null and b/apps/editor/public/icons/settings.png differ diff --git a/bun.lock b/bun.lock index 5bb8e242..6c8fffb4 100644 --- a/bun.lock +++ b/bun.lock @@ -174,6 +174,7 @@ "name": "@pascal-app/viewer", "version": "0.1.13", "dependencies": { + "polygon-clipping": "^0.15.7", "zustand": "^5", }, "devDependencies": { @@ -1314,6 +1315,8 @@ "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "polygon-clipping": ["polygon-clipping@0.15.7", "", { "dependencies": { "robust-predicates": "^3.0.2", "splaytree": "^3.1.0" } }, "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA=="], + "portless": ["portless@0.4.2", "", { "dependencies": { "chalk": "^5.3.0" }, "os": [ "linux", "darwin", ], "bin": { "portless": "dist/cli.js" } }, "sha512-/G3jIeD1XokoO9KY/lUGTV9irKz3tgx8yqHkz+hvj/86QeR219GvYFB+QEfpvjRlvvLVJa5vE7BkRfBZBe/lQg=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -1388,6 +1391,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -1438,6 +1443,8 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "splaytree": ["splaytree@3.2.3", "", {}, "sha512-7OXrNWzy6CK+r7Ch9OLPBDTKfB6XlWHjX4P0RU5B3IgFuWPeYN0XtRtlexGRjgbQxpfaUve6jTAwBGWuGntz/w=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], "stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="], diff --git a/packages/db/src/schema/projects/projects.ts b/packages/db/src/schema/projects/projects.ts index f8cb47f8..3aa5c4d3 100644 --- a/packages/db/src/schema/projects/projects.ts +++ b/packages/db/src/schema/projects/projects.ts @@ -20,6 +20,7 @@ export const projects = pgTable( metadata: t.jsonb('metadata'), // Community features isPrivate: t.boolean('is_private').notNull().default(true), + isEmpty: t.boolean('is_empty').notNull().default(true), showScansPublic: t.boolean('show_scans_public').notNull().default(true), showGuidesPublic: t.boolean('show_guides_public').notNull().default(true), views: t.integer('views').notNull().default(0), diff --git a/packages/db/supabase/migrations/20240218000001_create_feedback_table.sql b/packages/db/supabase/migrations/20240218000001_create_feedback_table.sql deleted file mode 100644 index 3c2124e4..00000000 --- a/packages/db/supabase/migrations/20240218000001_create_feedback_table.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Create feedback table -CREATE TABLE IF NOT EXISTS feedback ( - id TEXT NOT NULL PRIMARY KEY, - user_id TEXT, - message TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - --- Enable Row Level Security -ALTER TABLE feedback ENABLE ROW LEVEL SECURITY; - --- Allow anyone (authenticated or anonymous) to submit feedback -CREATE POLICY "Anyone can insert feedback" - ON feedback - FOR INSERT - TO anon, authenticated - WITH CHECK (true); - --- Allow service role full access (for admin review) -CREATE POLICY "Service role full access" - ON feedback - TO service_role - USING (true) - WITH CHECK (true); diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index c6daff18..acff721f 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -2,7 +2,9 @@ "extends": "@repo/typescript-config/base.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": "src", + "module": "ESNext", + "moduleResolution": "bundler" }, "include": ["src"], "exclude": ["node_modules", "dist"] diff --git a/packages/viewer/package.json b/packages/viewer/package.json index 508a27d2..c9d6e4cb 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -29,6 +29,7 @@ "three": "^0.183" }, "dependencies": { + "polygon-clipping": "^0.15.7", "zustand": "^5" }, "devDependencies": { diff --git a/packages/viewer/src/components/viewer/ground-occluder.tsx b/packages/viewer/src/components/viewer/ground-occluder.tsx new file mode 100644 index 00000000..91ffa83c --- /dev/null +++ b/packages/viewer/src/components/viewer/ground-occluder.tsx @@ -0,0 +1,70 @@ +import { useScene } from '@pascal-app/core' +import { useMemo } from 'react' +import * as THREE from 'three' +import useViewer from '../../store/use-viewer' +import polygonClipping from 'polygon-clipping' + +export const GroundOccluder = () => { + const theme = useViewer((state) => state.theme) + const bgColor = theme === 'dark' ? '#1f2433' : '#fafafa' + + const nodes = useScene((state) => state.nodes) + + const shape = useMemo(() => { + const s = new THREE.Shape() + const size = 100000 + // Create outer infinite plane + s.moveTo(-size, -size) + s.lineTo(size, -size) + s.lineTo(size, size) + s.lineTo(-size, size) + s.closePath() + + // Collect all polygons for slabs and zones + const polygons: [number, number][][] = [] + + Object.values(nodes).forEach((node) => { + if ((node.type === 'slab' || node.type === 'zone') && node.polygon && node.polygon.length >= 3) { + polygons.push(node.polygon as [number, number][]) + } + }) + + if (polygons.length > 0) { + // Format for polygon-clipping: [[[x, y], [x, y], ...]] + const multiPolygons = polygons.map(pts => { + const ring = pts.map(p => [p[0], -p[1]] as [number, number]) // Negate Y (which was Z) + return [ring] + }) + + // Union all polygons together to prevent artifacts from overlapping + const unionedPolygons = polygonClipping.union(multiPolygons[0]!, ...multiPolygons.slice(1)) + + // Add each resulting unioned polygon as a hole + for (const geom of unionedPolygons) { + // First ring in each geometry is the exterior ring + if (geom.length > 0) { + const ring = geom[0]! + const hole = new THREE.Path() + + if (ring.length > 0) { + hole.moveTo(ring[0]![0], ring[0]![1]) + for (let i = 1; i < ring.length; i++) { + hole.lineTo(ring[i]![0], ring[i]![1]) + } + hole.closePath() + s.holes.push(hole) + } + } + } + } + + return s + }, [nodes]) + + return ( + + + + + ) +} diff --git a/packages/viewer/src/components/viewer/index.tsx b/packages/viewer/src/components/viewer/index.tsx index 7f23decc..2b144668 100644 --- a/packages/viewer/src/components/viewer/index.tsx +++ b/packages/viewer/src/components/viewer/index.tsx @@ -1,22 +1,57 @@ 'use client' -import { CeilingSystem, DoorSystem, ItemSystem, RoofSystem, SlabSystem, WallSystem, WindowSystem } from '@pascal-app/core' +import { + CeilingSystem, + DoorSystem, + ItemSystem, + RoofSystem, + SlabSystem, + WallSystem, + WindowSystem, +} from '@pascal-app/core' import { Bvh } from '@react-three/drei' -import { Canvas, extend, type ThreeToJSXElements } from '@react-three/fiber' +import { Canvas, extend, type ThreeToJSXElements, useFrame } from '@react-three/fiber' +import { useEffect, useMemo, useRef } from 'react' import * as THREE from 'three/webgpu' +import useViewer from '../../store/use-viewer' import { GuideSystem } from '../../systems/guide/guide-system' import { LevelSystem } from '../../systems/level/level-system' import { ScanSystem } from '../../systems/scan/scan-system' import { WallCutout } from '../../systems/wall/wall-cutout' import { ZoneSystem } from '../../systems/zone/zone-system' import { SceneRenderer } from '../renderers/scene-renderer' +import { GroundOccluder } from './ground-occluder' import { Lights } from './lights' import PostProcessing from './post-processing' import { SelectionManager } from './selection-manager' import { ViewerCamera } from './viewer-camera' -import { useEffect } from 'react' -import useViewer from '../../store/use-viewer' +function AnimatedBackground({ isDark }: { isDark: boolean }) { + const targetColor = useMemo(() => new THREE.Color(), []) + const initialized = useRef(false) + + useFrame(({ scene }, delta) => { + const dt = Math.min(delta, 0.1) * 4 + const targetHex = isDark ? '#1f2433' : '#ffffff' + + if (!scene.background || !(scene.background instanceof THREE.Color)) { + scene.background = new THREE.Color(targetHex) + initialized.current = true + return + } + + if (!initialized.current) { + scene.background.set(targetHex) + initialized.current = true + return + } + + targetColor.set(targetHex) + scene.background.lerp(targetColor, dt) + }) + + return null +} declare module '@react-three/fiber' { interface ThreeElements extends ThreeToJSXElements {} @@ -30,8 +65,15 @@ interface ViewerProps { isEditor?: boolean } -const Viewer: React.FC = ({ children, selectionManager = 'default', isEditor = false }) => { +const Viewer: React.FC = ({ + children, + selectionManager = 'default', + isEditor = false, +}) => { const setIsEditor = useViewer((state) => state.setIsEditor) + const theme = useViewer((state) => state.theme) + + const bgColor = theme === 'dark' ? '#1f2433' : '#fafafa' useEffect(() => { setIsEditor(isEditor) @@ -40,7 +82,7 @@ const Viewer: React.FC = ({ children, selectionManager = 'default', return ( { const renderer = new THREE.WebGPURenderer(props as any) renderer.toneMapping = THREE.ACESFilmicToneMapping @@ -53,7 +95,8 @@ const Viewer: React.FC = ({ children, selectionManager = 'default', }} camera={{ position: [50, 50, 50], fov: 50 }} > - + + {/* (null) + const theme = useViewer((state) => state.theme) + const isDark = theme === 'dark' + + const light1Ref = useRef(null) const shadowCamera = useRef(null) const shadowCameraSize = 50 // The "area" around the camera to shadow - // useHelper(lightRef, DirectionalLightHelper, 1, 'red') - // useHelper(shadowCamera, CameraHelper) + const light2Ref = useRef(null) + const light3Ref = useRef(null) + const ambientRef = useRef(null) + + const initialized = useRef(false) + + const targets = useMemo(() => ({ + l1Color: new THREE.Color(), + l2Color: new THREE.Color(), + l3Color: new THREE.Color(), + ambColor: new THREE.Color(), + }), []) + + useFrame((_, delta) => { + // clamp delta to avoid huge jumps on tab switch + const dt = Math.min(delta, 0.1) * 4 + + if (!initialized.current) { + if (light1Ref.current) { + light1Ref.current.intensity = isDark ? 0.8 : 4 + light1Ref.current.color.set(isDark ? '#e0e5ff' : '#ffffff') + // @ts-ignore + if (light1Ref.current.shadow) light1Ref.current.shadow.intensity = isDark ? 0.8 : 0.4 + } + if (light2Ref.current) { + light2Ref.current.intensity = isDark ? 0.2 : 0.75 + light2Ref.current.color.set(isDark ? '#8090ff' : '#ffffff') + } + if (light3Ref.current) { + light3Ref.current.intensity = isDark ? 0.3 : 1 + light3Ref.current.color.set(isDark ? '#a0b0ff' : '#ffffff') + } + if (ambientRef.current) { + ambientRef.current.intensity = isDark ? 0.15 : 0.5 + ambientRef.current.color.set(isDark ? '#a0b0ff' : '#ffffff') + } + initialized.current = true + return + } + + if (light1Ref.current) { + light1Ref.current.intensity = THREE.MathUtils.lerp(light1Ref.current.intensity, isDark ? 0.8 : 4, dt) + targets.l1Color.set(isDark ? '#e0e5ff' : '#ffffff') + light1Ref.current.color.lerp(targets.l1Color, dt) + + if (light1Ref.current.shadow) { + // @ts-ignore + if (light1Ref.current.shadow.intensity !== undefined) { + // @ts-ignore + light1Ref.current.shadow.intensity = THREE.MathUtils.lerp(light1Ref.current.shadow.intensity, isDark ? 0.8 : 0.4, dt) + } + } + } + + if (light2Ref.current) { + light2Ref.current.intensity = THREE.MathUtils.lerp(light2Ref.current.intensity, isDark ? 0.2 : 0.75, dt) + targets.l2Color.set(isDark ? '#8090ff' : '#ffffff') + light2Ref.current.color.lerp(targets.l2Color, dt) + } + + if (light3Ref.current) { + light3Ref.current.intensity = THREE.MathUtils.lerp(light3Ref.current.intensity, isDark ? 0.3 : 1, dt) + targets.l3Color.set(isDark ? '#a0b0ff' : '#ffffff') + light3Ref.current.color.lerp(targets.l3Color, dt) + } + + if (ambientRef.current) { + ambientRef.current.intensity = THREE.MathUtils.lerp(ambientRef.current.intensity, isDark ? 0.15 : 0.5, dt) + targets.ambColor.set(isDark ? '#a0b0ff' : '#ffffff') + ambientRef.current.color.lerp(targets.ambColor, dt) + } + }) return ( <> - - {/* */} + ) } diff --git a/packages/viewer/src/store/use-viewer.ts b/packages/viewer/src/store/use-viewer.ts index e10beb1c..21603975 100644 --- a/packages/viewer/src/store/use-viewer.ts +++ b/packages/viewer/src/store/use-viewer.ts @@ -35,6 +35,9 @@ type ViewerState = { cameraMode: 'perspective' | 'orthographic' setCameraMode: (mode: 'perspective' | 'orthographic') => void + theme: 'light' | 'dark' + setTheme: (theme: 'light' | 'dark') => void + levelMode: 'stacked' | 'exploded' | 'solo' | 'manual' setLevelMode: (mode: 'stacked' | 'exploded' | 'solo' | 'manual') => void @@ -47,9 +50,12 @@ type ViewerState = { showGuides: boolean setShowGuides: (show: boolean) => void + showGrid: boolean + setShowGrid: (show: boolean) => void + projectId: string | null setProjectId: (id: string | null) => void - projectPreferences: Record + projectPreferences: Record // Smart selection update setSelection: (updates: Partial) => void @@ -77,6 +83,9 @@ const useViewer = create()( cameraMode: "perspective", setCameraMode: (mode) => set({ cameraMode: mode }), + theme: "light", + setTheme: (theme) => set({ theme }), + levelMode: "stacked", setLevelMode: (mode) => set({ levelMode: mode }), @@ -109,6 +118,19 @@ const useViewer = create()( return { showGuides: show, projectPreferences }; }), + showGrid: true, + setShowGrid: (show) => + set((state) => { + const projectPreferences = { ...(state.projectPreferences || {}) }; + if (state.projectId) { + projectPreferences[state.projectId] = { + ...(projectPreferences[state.projectId] || {}), + showGrid: show, + }; + } + return { showGrid: show, projectPreferences }; + }), + projectId: null, setProjectId: (id) => set((state) => { @@ -118,6 +140,7 @@ const useViewer = create()( projectId: id, showScans: prefs.showScans ?? true, showGuides: prefs.showGuides ?? true, + showGrid: prefs.showGrid ?? true, }; }), projectPreferences: {}, @@ -165,6 +188,7 @@ const useViewer = create()( name: 'viewer-preferences', partialize: (state) => ({ cameraMode: state.cameraMode, + theme: state.theme, levelMode: state.levelMode, wallMode: state.wallMode, projectPreferences: state.projectPreferences, diff --git a/supabase/migrations/20260227200237_brown_quasimodo.sql b/supabase/migrations/20260227200237_brown_quasimodo.sql new file mode 100644 index 00000000..029b9893 --- /dev/null +++ b/supabase/migrations/20260227200237_brown_quasimodo.sql @@ -0,0 +1 @@ +ALTER TABLE "projects" ADD COLUMN "is_empty" boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/supabase/migrations/20260227204328_backfill_empty_projects.sql b/supabase/migrations/20260227204328_backfill_empty_projects.sql new file mode 100644 index 00000000..84415986 --- /dev/null +++ b/supabase/migrations/20260227204328_backfill_empty_projects.sql @@ -0,0 +1,38 @@ +-- Backfill script to update the is_empty flag for existing projects +-- This can be run multiple times safely as logic evolves + +UPDATE projects p +SET is_empty = false +FROM ( + -- 1. Get the latest model for each project + SELECT DISTINCT ON (project_id) project_id, scene_graph + FROM projects_models + WHERE deleted_at IS NULL + ORDER BY project_id, version DESC, created_at DESC +) latest_model +WHERE p.id = latest_model.project_id +AND p.is_empty = true -- Only process projects that are currently marked empty +AND latest_model.scene_graph IS NOT NULL +AND latest_model.scene_graph->'nodes' IS NOT NULL +AND ( + -- Condition 1: More than 3 nodes + (SELECT count(*) FROM jsonb_object_keys(latest_model.scene_graph->'nodes')) > 3 + + OR + + -- Condition 2 & 3: Iterate through nodes to check type and children + EXISTS ( + SELECT 1 + FROM jsonb_each(latest_model.scene_graph->'nodes') AS n(key, value) + WHERE + -- Not a default node type + (n.value->>'type' NOT IN ('site', 'building', 'level')) + OR + -- Or is a level node with > 0 children + ( + n.value->>'type' = 'level' + AND jsonb_typeof(n.value->'children') = 'array' + AND jsonb_array_length(n.value->'children') > 0 + ) + ) +); \ No newline at end of file diff --git a/supabase/migrations/meta/20260227200237_snapshot.json b/supabase/migrations/meta/20260227200237_snapshot.json new file mode 100644 index 00000000..82203ee1 --- /dev/null +++ b/supabase/migrations/meta/20260227200237_snapshot.json @@ -0,0 +1,1135 @@ +{ + "id": "1e54bffc-eb81-4d5f-b2f7-0f006e46e939", + "prevId": "62f78c14-f7f8-423a-be0f-c712160251f3", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.auth_accounts": { + "name": "auth_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_accounts_user_id_auth_users_id_fk": { + "name": "auth_accounts_user_id_auth_users_id_fk", + "tableFrom": "auth_accounts", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_jwks": { + "name": "auth_jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_sessions": { + "name": "auth_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_project_id": { + "name": "active_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_sessions_user_id_auth_users_id_fk": { + "name": "auth_sessions_user_id_auth_users_id_fk", + "tableFrom": "auth_sessions", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auth_sessions_impersonated_by_auth_users_id_fk": { + "name": "auth_sessions_impersonated_by_auth_users_id_fk", + "tableFrom": "auth_sessions", + "tableTo": "auth_users", + "columnsFrom": [ + "impersonated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_users": { + "name": "auth_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "x_url": { + "name": "x_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_url": { + "name": "youtube_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_notifications": { + "name": "email_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "role": { + "name": "role", + "type": "auth_user_roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_unique_index": { + "name": "email_unique_index", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "username_unique_index": { + "name": "username_unique_index", + "columns": [ + { + "expression": "lower(\"username\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_verifications": { + "name": "auth_verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_index": { + "name": "verification_identifier_index", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.feedback": { + "name": "feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scene_graph": { + "name": "scene_graph", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects_addresses": { + "name": "projects_addresses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "street_number": { + "name": "street_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route_short": { + "name": "route_short", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "neighborhood": { + "name": "neighborhood", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_long": { + "name": "state_long", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code_suffix": { + "name": "postal_code_suffix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_long": { + "name": "country_long", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "raw_json": { + "name": "raw_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "address_components_unique": { + "name": "address_components_unique", + "nullsNotDistinct": false, + "columns": [ + "street_number", + "route", + "city", + "state", + "postal_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.project_assets": { + "name": "project_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_assets_project_id_projects_id_fk": { + "name": "project_assets_project_id_projects_id_fk", + "tableFrom": "project_assets", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects_likes": { + "name": "projects_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_likes_project_id_projects_id_fk": { + "name": "projects_likes_project_id_projects_id_fk", + "tableFrom": "projects_likes", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_likes_project_user_unique": { + "name": "projects_likes_project_user_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects_models": { + "name": "projects_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft": { + "name": "draft", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scene_graph": { + "name": "scene_graph", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_models_project_id_projects_id_fk": { + "name": "projects_models_project_id_projects_id_fk", + "tableFrom": "projects_models", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_id": { + "name": "address_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details_json": { + "name": "details_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_empty": { + "name": "is_empty", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_scans_public": { + "name": "show_scans_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_guides_public": { + "name": "show_guides_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "likes": { + "name": "likes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_address_idx": { + "name": "project_address_idx", + "columns": [ + { + "expression": "address_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_idx": { + "name": "project_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_is_private_idx": { + "name": "project_is_private_idx", + "columns": [ + { + "expression": "is_private", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_views_idx": { + "name": "project_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_likes_idx": { + "name": "project_likes_idx", + "columns": [ + { + "expression": "likes", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_address_id_projects_addresses_id_fk": { + "name": "projects_address_id_projects_addresses_id_fk", + "tableFrom": "projects", + "tableTo": "projects_addresses", + "columnsFrom": [ + "address_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "projects_owner_id_auth_users_id_fk": { + "name": "projects_owner_id_auth_users_id_fk", + "tableFrom": "projects", + "tableTo": "auth_users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": { + "public.auth_user_roles": { + "name": "auth_user_roles", + "schema": "public", + "values": [ + "user", + "admin" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/supabase/migrations/meta/20260227204328_snapshot.json b/supabase/migrations/meta/20260227204328_snapshot.json new file mode 100644 index 00000000..9b5367ca --- /dev/null +++ b/supabase/migrations/meta/20260227204328_snapshot.json @@ -0,0 +1,1135 @@ +{ + "id": "b7bcfd85-59f4-4979-aa8c-69dcb20c4b32", + "prevId": "1e54bffc-eb81-4d5f-b2f7-0f006e46e939", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.auth_accounts": { + "name": "auth_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_accounts_user_id_auth_users_id_fk": { + "name": "auth_accounts_user_id_auth_users_id_fk", + "tableFrom": "auth_accounts", + "columnsFrom": [ + "user_id" + ], + "tableTo": "auth_users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_jwks": { + "name": "auth_jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_sessions": { + "name": "auth_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_project_id": { + "name": "active_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_sessions_user_id_auth_users_id_fk": { + "name": "auth_sessions_user_id_auth_users_id_fk", + "tableFrom": "auth_sessions", + "columnsFrom": [ + "user_id" + ], + "tableTo": "auth_users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "auth_sessions_impersonated_by_auth_users_id_fk": { + "name": "auth_sessions_impersonated_by_auth_users_id_fk", + "tableFrom": "auth_sessions", + "columnsFrom": [ + "impersonated_by" + ], + "tableTo": "auth_users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_users": { + "name": "auth_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "x_url": { + "name": "x_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "youtube_url": { + "name": "youtube_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_notifications": { + "name": "email_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "role": { + "name": "role", + "type": "auth_user_roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_unique_index": { + "name": "email_unique_index", + "columns": [ + { + "expression": "lower(\"email\")", + "isExpression": true, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + }, + "username_unique_index": { + "name": "username_unique_index", + "columns": [ + { + "expression": "lower(\"username\")", + "isExpression": true, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.auth_verifications": { + "name": "auth_verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_index": { + "name": "verification_identifier_index", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.feedback": { + "name": "feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scene_graph": { + "name": "scene_graph", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects_addresses": { + "name": "projects_addresses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "street_number": { + "name": "street_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route_short": { + "name": "route_short", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "neighborhood": { + "name": "neighborhood", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_long": { + "name": "state_long", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code_suffix": { + "name": "postal_code_suffix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_long": { + "name": "country_long", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "raw_json": { + "name": "raw_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "address_components_unique": { + "name": "address_components_unique", + "columns": [ + "street_number", + "route", + "city", + "state", + "postal_code" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.project_assets": { + "name": "project_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_assets_project_id_projects_id_fk": { + "name": "project_assets_project_id_projects_id_fk", + "tableFrom": "project_assets", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects_likes": { + "name": "projects_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_likes_project_id_projects_id_fk": { + "name": "projects_likes_project_id_projects_id_fk", + "tableFrom": "projects_likes", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_likes_project_user_unique": { + "name": "projects_likes_project_user_unique", + "columns": [ + "project_id", + "user_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects_models": { + "name": "projects_models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft": { + "name": "draft", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scene_graph": { + "name": "scene_graph", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_models_project_id_projects_id_fk": { + "name": "projects_models_project_id_projects_id_fk", + "tableFrom": "projects_models", + "columnsFrom": [ + "project_id" + ], + "tableTo": "projects", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_id": { + "name": "address_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details_json": { + "name": "details_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_empty": { + "name": "is_empty", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_scans_public": { + "name": "show_scans_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_guides_public": { + "name": "show_guides_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "likes": { + "name": "likes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_address_idx": { + "name": "project_address_idx", + "columns": [ + { + "expression": "address_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "project_owner_idx": { + "name": "project_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "project_is_private_idx": { + "name": "project_is_private_idx", + "columns": [ + { + "expression": "is_private", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "project_views_idx": { + "name": "project_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "project_likes_idx": { + "name": "project_likes_idx", + "columns": [ + { + "expression": "likes", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "projects_address_id_projects_addresses_id_fk": { + "name": "projects_address_id_projects_addresses_id_fk", + "tableFrom": "projects", + "columnsFrom": [ + "address_id" + ], + "tableTo": "projects_addresses", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "projects_owner_id_auth_users_id_fk": { + "name": "projects_owner_id_auth_users_id_fk", + "tableFrom": "projects", + "columnsFrom": [ + "owner_id" + ], + "tableTo": "auth_users", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": { + "public.auth_user_roles": { + "name": "auth_user_roles", + "schema": "public", + "values": [ + "user", + "admin" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/supabase/migrations/meta/_journal.json b/supabase/migrations/meta/_journal.json index 52458a45..4b2e3583 100644 --- a/supabase/migrations/meta/_journal.json +++ b/supabase/migrations/meta/_journal.json @@ -50,6 +50,20 @@ "when": 1772216042000, "tag": "20260227173402_create_missing_storage_buckets", "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1772222557281, + "tag": "20260227200237_brown_quasimodo", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1772225008787, + "tag": "20260227204328_backfill_empty_projects", + "breakpoints": true } ] } \ No newline at end of file