commit
38cd9215b0
254
SETUP.md
Normal file
254
SETUP.md
Normal file
@ -0,0 +1,254 @@
|
||||
# Pascal Editor - Setup Guide
|
||||
|
||||
This guide will help you set up the Pascal Editor with authentication and database integration.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+ or Bun 1.3+
|
||||
- Docker Desktop (for running Supabase locally)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
This installs the Supabase CLI as a dev dependency - no need for global installation!
|
||||
|
||||
### 2. Start Supabase Local Development
|
||||
|
||||
```bash
|
||||
bun db:start
|
||||
```
|
||||
|
||||
This will start a local Supabase instance. You'll see output like:
|
||||
|
||||
```
|
||||
API URL: http://127.0.0.1:54321
|
||||
DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
||||
Studio URL: http://127.0.0.1:54323
|
||||
Anon key: eyJh...
|
||||
Service role key: eyJh...
|
||||
```
|
||||
|
||||
### 4. Configure Environment Variables
|
||||
|
||||
Create `apps/editor/.env.local` with the following variables:
|
||||
|
||||
```bash
|
||||
# Database Connection (Supabase local)
|
||||
POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
||||
|
||||
# Supabase Configuration
|
||||
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
|
||||
|
||||
# Better Auth (generate your own secret with: openssl rand -base64 32)
|
||||
BETTER_AUTH_SECRET=<generate_with_command_below>
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# Google Maps (optional, for address search)
|
||||
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=<your_google_maps_key>
|
||||
```
|
||||
|
||||
Generate a secret for `BETTER_AUTH_SECRET`:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
### 5. Run Database Migrations
|
||||
|
||||
```bash
|
||||
bun db:reset
|
||||
```
|
||||
|
||||
This will create all necessary tables for authentication and properties.
|
||||
|
||||
### 6. Start the Development Server
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
The editor will be available at http://localhost:3000
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── apps/
|
||||
│ └── editor/ # Next.js editor application
|
||||
│ ├── app/
|
||||
│ │ └── api/auth/ # Better Auth API routes
|
||||
│ ├── components/ # UI components
|
||||
│ └── features/
|
||||
│ └── cloud-sync/ # Cloud sync feature
|
||||
├── packages/
|
||||
│ ├── auth/ # @pascal-app/auth - Authentication package
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── server.ts # Better Auth server config
|
||||
│ │ │ └── client.ts # Better Auth client
|
||||
│ │ └── README.md
|
||||
│ ├── db/ # @pascal-app/db - Database package
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── client.ts # Supabase client (with RLS)
|
||||
│ │ │ ├── server.ts # Supabase admin client
|
||||
│ │ │ └── types.ts # Database types
|
||||
│ │ ├── supabase/
|
||||
│ │ │ ├── config.toml
|
||||
│ │ │ └── migrations/ # SQL migrations
|
||||
│ │ └── README.md
|
||||
│ ├── core/ # @pascal-app/core - Core editor logic
|
||||
│ ├── viewer/ # @pascal-app/viewer - 3D viewer
|
||||
│ └── ui/ # @repo/ui - Shared UI components
|
||||
└── turbo.json
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Auth Tables (Better Auth)
|
||||
|
||||
- **users** - User accounts with email and profile
|
||||
- **sessions** - Active authentication sessions
|
||||
- **accounts** - OAuth provider accounts (for future use)
|
||||
- **verification_tokens** - Magic link tokens
|
||||
|
||||
### Application Tables
|
||||
|
||||
- **properties** - User properties
|
||||
- `id`: Property ID
|
||||
- `name`: Property name
|
||||
- `owner_id`: User ID (foreign key to users)
|
||||
|
||||
- **properties_addresses** - Property addresses with Google Maps data
|
||||
- `id`: Address ID
|
||||
- `property_id`: Property ID (foreign key)
|
||||
- `formatted_address`: Full address
|
||||
- `latitude`, `longitude`: GPS coordinates
|
||||
- Plus detailed address components (street, city, state, etc.)
|
||||
|
||||
- **properties_models** - Scene graph models (versions)
|
||||
- `id`: Model ID
|
||||
- `property_id`: Property ID (foreign key)
|
||||
- `name`: Model name
|
||||
- `version`: Version number
|
||||
- `draft`: Draft status
|
||||
- `scene_graph`: JSONB scene graph data
|
||||
|
||||
## Features
|
||||
|
||||
### Authentication
|
||||
|
||||
- **Magic Link Sign-In**: Passwordless authentication via email
|
||||
- **Session Management**: 7-day sessions with automatic refresh
|
||||
- **Cookie-based**: Secure httpOnly cookies
|
||||
|
||||
### Property Management
|
||||
|
||||
- **Create Properties**: Add properties with real-world addresses
|
||||
- **Google Maps Integration**: Address autocomplete and geocoding
|
||||
- **Switch Properties**: Seamlessly switch between properties
|
||||
|
||||
### Scene Management
|
||||
|
||||
- **Auto-Save**: Changes saved every 2 seconds
|
||||
- **Scene Loading**: Automatic scene loading when switching properties
|
||||
- **Version Control**: Models are versioned for future rollback support
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Making Database Changes
|
||||
|
||||
1. Create a new migration:
|
||||
```bash
|
||||
cd packages/db
|
||||
supabase migration new your_migration_name
|
||||
```
|
||||
|
||||
2. Edit the migration file in `supabase/migrations/`
|
||||
|
||||
3. Apply the migration:
|
||||
```bash
|
||||
supabase db reset
|
||||
```
|
||||
|
||||
### Updating Database Types
|
||||
|
||||
After changing the database schema, regenerate TypeScript types:
|
||||
|
||||
1. Update `packages/db/src/types.ts` to match your new schema
|
||||
2. Run `bun install` to update type checking
|
||||
|
||||
### Testing Authentication
|
||||
|
||||
1. Start the editor: `bun dev`
|
||||
2. Click "Save to cloud" button
|
||||
3. Enter your email
|
||||
4. Check console for magic link (not sent via email in development)
|
||||
5. Click the link to authenticate
|
||||
|
||||
## Supabase Studio
|
||||
|
||||
Access the local Supabase Studio at: http://127.0.0.1:54323
|
||||
|
||||
Use this to:
|
||||
- Browse and edit tables
|
||||
- Run SQL queries
|
||||
- View logs
|
||||
- Manage RLS policies
|
||||
- Test database functions
|
||||
|
||||
## Production Deployment
|
||||
|
||||
For production deployment:
|
||||
|
||||
1. Create a Supabase project at https://supabase.com
|
||||
2. Get your production database connection string
|
||||
3. Update environment variables in your hosting platform
|
||||
4. Link and push migrations:
|
||||
```bash
|
||||
cd packages/db
|
||||
bunx supabase link --project-ref your-project-ref
|
||||
bunx supabase db push
|
||||
```
|
||||
5. Configure email provider in `packages/auth/src/server.ts`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Missing POSTGRES_URL" error
|
||||
|
||||
Make sure you've set `POSTGRES_URL` in `apps/editor/.env.local` to your Supabase connection string.
|
||||
|
||||
### Supabase not starting
|
||||
|
||||
Try stopping and restarting:
|
||||
```bash
|
||||
bun db:stop
|
||||
bun db:start
|
||||
```
|
||||
|
||||
### Migration errors
|
||||
|
||||
Reset the database:
|
||||
```bash
|
||||
cd packages/db
|
||||
supabase db reset
|
||||
```
|
||||
|
||||
### Auth not working
|
||||
|
||||
1. Check that Better Auth API route exists at `apps/editor/app/api/auth/[...all]/route.ts`
|
||||
2. Verify `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` are set
|
||||
3. Check console for magic link URLs in development
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Configure email provider for magic links
|
||||
- Add OAuth providers (Google, GitHub, etc.)
|
||||
- Set up production Supabase project
|
||||
- Configure RLS policies for additional security
|
||||
- Add more property features (sharing, collaboration, etc.)
|
||||
11
apps/editor/app/api/auth/[...all]/route.ts
Normal file
11
apps/editor/app/api/auth/[...all]/route.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Better Auth API route handler
|
||||
* Handles all /api/auth/* routes for authentication
|
||||
*/
|
||||
|
||||
import { toNextJsHandler } from 'better-auth/next-js'
|
||||
import { auth } from '@/lib/auth'
|
||||
|
||||
const { GET, POST } = toNextJsHandler(auth)
|
||||
|
||||
export { GET, POST }
|
||||
@ -4,9 +4,11 @@ import { initSpaceDetectionSync, initSpatialGridSync, useScene } from '@pascal-a
|
||||
import { Viewer } from '@pascal-app/viewer'
|
||||
import { useKeyboard } from '@/hooks/use-keyboard'
|
||||
import useEditor from '@/store/use-editor'
|
||||
import { usePropertyScene } from '@/features/community/lib/models/hooks'
|
||||
import { ZoneSystem } from '../systems/zone/zone-system'
|
||||
import { ToolManager } from '../tools/tool-manager'
|
||||
import { ActionMenu } from '../ui/action-menu'
|
||||
import { CloudSaveButton } from '@/features/community/components/cloud-save-button'
|
||||
import { PanelManager } from '../ui/panels/panel-manager'
|
||||
import { SidebarProvider } from '../ui/primitives/sidebar'
|
||||
import { AppSidebar } from '../ui/sidebar/app-sidebar'
|
||||
@ -15,8 +17,8 @@ import { ExportManager } from './export-manager'
|
||||
import { Grid } from './grid'
|
||||
import { SelectionManager } from './selection-manager'
|
||||
|
||||
// Load default scene initially (will be replaced when property loads)
|
||||
useScene.getState().loadScene()
|
||||
console.log('Loaded scene in editor')
|
||||
initSpatialGridSync()
|
||||
initSpaceDetectionSync(useScene, useEditor)
|
||||
|
||||
@ -27,6 +29,7 @@ export default function Editor() {
|
||||
<div className="w-full h-full">
|
||||
<ActionMenu />
|
||||
<PanelManager />
|
||||
<CloudSaveButton />
|
||||
|
||||
<SidebarProvider className="fixed z-20">
|
||||
<AppSidebar />
|
||||
|
||||
198
apps/editor/features/community/README.md
Normal file
198
apps/editor/features/community/README.md
Normal file
@ -0,0 +1,198 @@
|
||||
# Community Feature
|
||||
|
||||
This directory contains the **optional** community features (cloud synchronization and authentication) for the Pascal Editor. This feature is specific to the Pascal platform and can be safely removed if you're using the editor standalone.
|
||||
|
||||
## What This Does
|
||||
|
||||
The community feature provides:
|
||||
|
||||
- **Authentication** - Sign in with magic link via Better Auth
|
||||
- **Property Management** - Create and manage properties with Google Maps address search
|
||||
- **Scene Loading** - Automatically load property scenes from the database when a property is selected
|
||||
- **Auto-Save** - Automatically save scene changes to the database (2-second debounce)
|
||||
- **Database Sync** - Save and load editor state from a PostgreSQL database via Supabase
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
features/community/
|
||||
├── lib/
|
||||
│ ├── auth/
|
||||
│ │ ├── client.ts # Re-exports from @pascal-app/auth
|
||||
│ │ ├── server.ts # Server-side session handling
|
||||
│ │ └── hooks.ts # useAuth React hook
|
||||
│ ├── properties/
|
||||
│ │ ├── actions.ts # Server actions for CRUD operations
|
||||
│ │ ├── types.ts # TypeScript types for properties
|
||||
│ │ ├── hooks.ts # Property React hooks
|
||||
│ │ └── store.ts # Zustand store for property state
|
||||
│ ├── models/
|
||||
│ │ ├── actions.ts # Scene model CRUD operations
|
||||
│ │ └── hooks.ts # Scene loading and auto-save hooks
|
||||
│ ├── database/
|
||||
│ │ └── server.ts # Re-exports from @pascal-app/db
|
||||
│ └── utils/
|
||||
│ └── id-generator.ts # nanoid-based ID generation
|
||||
├── components/
|
||||
│ ├── cloud-save-button.tsx # Main UI entry point (top-right button)
|
||||
│ ├── sign-in-dialog.tsx # Magic link sign-in dialog
|
||||
│ ├── profile-dropdown.tsx # User profile menu
|
||||
│ ├── property-dropdown.tsx # Property selector dropdown
|
||||
│ ├── new-property-dialog.tsx # Create new property dialog
|
||||
│ └── google-address-search.tsx # Google Maps autocomplete
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Authentication Flow
|
||||
1. User clicks "Save to cloud" button
|
||||
2. Signs in with magic link (email-based, no password)
|
||||
3. Better Auth session is stored in cookies
|
||||
4. Server actions validate session using Better Auth API
|
||||
|
||||
### Property Management
|
||||
1. User creates a property with a real-world address (Google Maps)
|
||||
2. Address and property are saved to PostgreSQL via Supabase
|
||||
3. Properties are associated with the authenticated user
|
||||
4. User can switch between properties
|
||||
|
||||
### Scene Management
|
||||
1. When a property is selected, its scene is loaded from `properties_models` table
|
||||
2. If no scene exists, loads default empty scene
|
||||
3. Scene changes are auto-saved every 2 seconds (debounced)
|
||||
4. Updates existing model (highest version) instead of creating new ones
|
||||
5. Scene graph includes all nodes and hierarchy
|
||||
|
||||
### Database Integration
|
||||
- Uses Supabase (PostgreSQL) for database access
|
||||
- Better Auth manages authentication tables directly
|
||||
- Server actions use service role key to bypass RLS
|
||||
- Permissions enforced by filtering on `owner_id`
|
||||
- Tables: `users`, `sessions`, `properties`, `properties_addresses`, `properties_models`
|
||||
|
||||
## Required Environment Variables
|
||||
|
||||
```bash
|
||||
# Database Connection
|
||||
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
||||
|
||||
# Supabase Configuration
|
||||
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key_here
|
||||
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here
|
||||
|
||||
# Better Auth
|
||||
BETTER_AUTH_SECRET=<generate_with_openssl_rand_base64_32>
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# Google Maps API Key (for address search)
|
||||
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your_google_maps_key_here
|
||||
```
|
||||
|
||||
Generate `BETTER_AUTH_SECRET`:
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
The community feature requires these packages:
|
||||
|
||||
```json
|
||||
{
|
||||
"better-auth": "^1.4.18",
|
||||
"@supabase/supabase-js": "^2.95.3",
|
||||
"@react-google-maps/api": "^2.20.8",
|
||||
"nanoid": "^5.1.6"
|
||||
}
|
||||
```
|
||||
|
||||
## How to Remove (For Open Source Users)
|
||||
|
||||
If you want to use the editor without community features:
|
||||
|
||||
### 1. Delete this directory
|
||||
```bash
|
||||
rm -rf features/community
|
||||
```
|
||||
|
||||
### 2. Remove the CloudSaveButton from the editor
|
||||
Edit `components/editor/index.tsx`:
|
||||
```diff
|
||||
- import { CloudSaveButton } from '@/features/community/components/cloud-save-button'
|
||||
|
||||
export default function Editor() {
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<ActionMenu />
|
||||
<PanelManager />
|
||||
- <CloudSaveButton />
|
||||
```
|
||||
|
||||
### 3. Remove dependencies (optional)
|
||||
Edit `package.json`:
|
||||
```diff
|
||||
- "better-auth": "^1.4.18",
|
||||
- "@supabase/supabase-js": "^2.95.3",
|
||||
- "@react-google-maps/api": "^2.20.8",
|
||||
- "nanoid": "^5.1.6"
|
||||
```
|
||||
|
||||
### 4. Remove environment variables
|
||||
Delete from `.env.local` and `.env.example`:
|
||||
```diff
|
||||
- NEXT_PUBLIC_API_URL=...
|
||||
- NEXT_PUBLIC_SUPABASE_URL=...
|
||||
- NEXT_PUBLIC_SUPABASE_ANON_KEY=...
|
||||
- SUPABASE_SERVICE_ROLE_KEY=...
|
||||
- NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=...
|
||||
```
|
||||
|
||||
That's it! The editor will work as a standalone application without any cloud features.
|
||||
|
||||
## Backend Requirements
|
||||
|
||||
This feature requires:
|
||||
- Supabase local development instance
|
||||
- PostgreSQL database with the following tables:
|
||||
- `users` - User accounts (Better Auth)
|
||||
- `sessions` - Authentication sessions (Better Auth)
|
||||
- `verification_tokens` - Magic link tokens (Better Auth)
|
||||
- `properties` - Property records
|
||||
- `properties_addresses` - Property addresses
|
||||
- `properties_models` - Scene graph models
|
||||
- Database migrations are managed in `packages/db/supabase/migrations/`
|
||||
|
||||
## Development
|
||||
|
||||
To work on this feature:
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
2. Start Supabase local development:
|
||||
```bash
|
||||
bun db:start
|
||||
```
|
||||
3. Run database migrations:
|
||||
```bash
|
||||
bun db:reset
|
||||
```
|
||||
4. Configure all environment variables in `apps/editor/.env.local`
|
||||
5. Run the editor: `bun dev`
|
||||
|
||||
The editor will be available at `http://localhost:3000`.
|
||||
|
||||
For detailed setup instructions, see [SETUP.md](../../../SETUP.md) in the root directory.
|
||||
|
||||
## Notes
|
||||
|
||||
- This feature uses **server actions** (Next.js App Router) for all database operations
|
||||
- Authentication is handled by **Better Auth** with magic link support
|
||||
- Better Auth server is configured in `packages/auth` and mounted at `/api/auth/*`
|
||||
- The editor queries the database directly using Supabase with service role key
|
||||
- IDs are generated using nanoid with custom alphabet
|
||||
- Scene state is managed with a **Zustand store** for reliable property switching
|
||||
- Scene changes are auto-saved with 2-second debouncing to the currently selected property
|
||||
@ -0,0 +1,64 @@
|
||||
'use client'
|
||||
|
||||
import { Cloud } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAuth } from '../lib/auth/hooks'
|
||||
import { usePropertyStore } from '../lib/properties/store'
|
||||
import { ProfileDropdown } from './profile-dropdown'
|
||||
import { PropertyDropdown } from './property-dropdown'
|
||||
import { SignInDialog } from './sign-in-dialog'
|
||||
|
||||
/**
|
||||
* CloudSaveButton - Shows authentication state and property management
|
||||
*
|
||||
* Not authenticated: Shows "Save to cloud" button
|
||||
* Authenticated: Shows PropertyDropdown and ProfileDropdown
|
||||
*/
|
||||
export function CloudSaveButton() {
|
||||
const { isAuthenticated, isLoading } = useAuth()
|
||||
const [isSignInDialogOpen, setIsSignInDialogOpen] = useState(false)
|
||||
const initialize = usePropertyStore(state => state.initialize)
|
||||
|
||||
// Initialize property store when authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
initialize()
|
||||
}
|
||||
}, [isAuthenticated, initialize])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="pointer-events-auto fixed top-4 right-4 z-50">
|
||||
<div className="flex items-center gap-2 rounded-lg border border-border bg-background/95 px-3 py-2 shadow-lg backdrop-blur-md">
|
||||
<div className="h-4 w-4 animate-pulse rounded-full bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<>
|
||||
<div className="pointer-events-auto fixed top-4 right-4 z-50">
|
||||
<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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto fixed top-4 right-4 z-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<PropertyDropdown />
|
||||
<ProfileDropdown />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
'use client'
|
||||
|
||||
import { Autocomplete, LoadScript } from '@react-google-maps/api'
|
||||
import { MapPin } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
const libraries: ('places')[] = ['places']
|
||||
|
||||
interface AddressComponents {
|
||||
streetNumber?: string
|
||||
route?: string
|
||||
city?: string
|
||||
state?: string
|
||||
postalCode?: string
|
||||
country?: string
|
||||
center: [number, number]
|
||||
formattedAddress: string
|
||||
}
|
||||
|
||||
interface GoogleAddressSearchProps {
|
||||
onAddressSelect: (address: AddressComponents) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function GoogleAddressSearch({ onAddressSelect, disabled }: GoogleAddressSearchProps) {
|
||||
const [autocomplete, setAutocomplete] = useState<google.maps.places.Autocomplete | null>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY
|
||||
|
||||
// Fix Google Maps autocomplete dropdown z-index and pointer events to work with dialog
|
||||
useEffect(() => {
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
.pac-container {
|
||||
z-index: 9999 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
return () => {
|
||||
document.head.removeChild(style)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!apiKey) {
|
||||
return (
|
||||
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm">
|
||||
<p className="font-medium">Google Maps API Key Missing</p>
|
||||
<p className="mt-1 text-xs">
|
||||
Add NEXT_PUBLIC_GOOGLE_MAPS_API_KEY to your .env.local file
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const onLoad = (autocompleteInstance: google.maps.places.Autocomplete) => {
|
||||
setAutocomplete(autocompleteInstance)
|
||||
}
|
||||
|
||||
const onPlaceChanged = () => {
|
||||
if (!autocomplete) return
|
||||
|
||||
const place = autocomplete.getPlace()
|
||||
if (!place.geometry?.location || !place.address_components) return
|
||||
|
||||
const components: AddressComponents = {
|
||||
center: [place.geometry.location.lng(), place.geometry.location.lat()],
|
||||
formattedAddress: place.formatted_address || '',
|
||||
}
|
||||
|
||||
// Parse address components
|
||||
for (const component of place.address_components) {
|
||||
const types = component.types
|
||||
|
||||
if (types.includes('street_number')) {
|
||||
components.streetNumber = component.long_name
|
||||
} else if (types.includes('route')) {
|
||||
components.route = component.long_name
|
||||
} else if (types.includes('locality')) {
|
||||
components.city = component.long_name
|
||||
} else if (types.includes('administrative_area_level_1')) {
|
||||
components.state = component.short_name
|
||||
} else if (types.includes('postal_code')) {
|
||||
components.postalCode = component.long_name
|
||||
} else if (types.includes('country')) {
|
||||
components.country = component.short_name
|
||||
}
|
||||
}
|
||||
|
||||
onAddressSelect(components)
|
||||
}
|
||||
|
||||
return (
|
||||
<LoadScript googleMapsApiKey={apiKey} libraries={libraries}>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 font-medium text-sm">
|
||||
<MapPin className="h-4 w-4" />
|
||||
Property Address
|
||||
</label>
|
||||
<Autocomplete onLoad={onLoad} onPlaceChanged={onPlaceChanged}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={disabled}
|
||||
placeholder="Search for an address..."
|
||||
type="text"
|
||||
/>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
</LoadScript>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
'use client'
|
||||
|
||||
import { X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { createProperty } from '../lib/properties/actions'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/primitives/dialog'
|
||||
import { GoogleAddressSearch } from './google-address-search'
|
||||
|
||||
interface NewPropertyDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSuccess?: (propertyId: string) => void
|
||||
}
|
||||
|
||||
interface AddressData {
|
||||
streetNumber?: string
|
||||
route?: string
|
||||
city?: string
|
||||
state?: string
|
||||
postalCode?: string
|
||||
country?: string
|
||||
center: [number, number]
|
||||
formattedAddress: string
|
||||
}
|
||||
|
||||
/**
|
||||
* NewPropertyDialog - Dialog for creating a new property with Google Maps address search
|
||||
*/
|
||||
export function NewPropertyDialog({ open, onOpenChange, onSuccess }: NewPropertyDialogProps) {
|
||||
const [address, setAddress] = useState<AddressData | null>(null)
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleAddressSelect = (addressData: AddressData) => {
|
||||
setAddress(addressData)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
|
||||
if (!address) {
|
||||
setError('Please select an address')
|
||||
return
|
||||
}
|
||||
|
||||
setIsCreating(true)
|
||||
|
||||
try {
|
||||
// Use formatted address as property name (like monorepo)
|
||||
const result = await createProperty({
|
||||
name: address.formattedAddress,
|
||||
center: address.center,
|
||||
streetNumber: address.streetNumber,
|
||||
route: address.route,
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
postalCode: address.postalCode,
|
||||
country: address.country || 'US',
|
||||
})
|
||||
|
||||
if (result.success && result.data) {
|
||||
onOpenChange(false)
|
||||
setAddress(null)
|
||||
onSuccess?.(result.data.id)
|
||||
} else {
|
||||
setError(result.error || 'Failed to create property')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unexpected error occurred')
|
||||
} finally {
|
||||
setIsCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isCreating) {
|
||||
onOpenChange(false)
|
||||
setAddress(null)
|
||||
setError(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose} modal={false}>
|
||||
<DialogContent
|
||||
className="sm:max-w-[500px]"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Property</DialogTitle>
|
||||
<button
|
||||
className="absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 disabled:pointer-events-none"
|
||||
disabled={isCreating}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
</DialogHeader>
|
||||
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
{/* Google Maps Address Search */}
|
||||
<GoogleAddressSearch onAddressSelect={handleAddressSelect} disabled={isCreating} />
|
||||
|
||||
{/* Show selected address */}
|
||||
{address && (
|
||||
<div className="rounded-md border border-border bg-muted/30 p-3 text-sm">
|
||||
<p className="font-medium">Selected Address:</p>
|
||||
<p className="mt-1 text-muted-foreground">{address.formattedAddress}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
className="rounded-md border border-input px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
|
||||
disabled={isCreating}
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md bg-primary px-4 py-2 text-primary-foreground text-sm transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
disabled={isCreating || !address}
|
||||
type="submit"
|
||||
>
|
||||
{isCreating ? 'Creating...' : 'Create Property'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import { useAuth } from '../lib/auth/hooks'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/primitives/dropdown-menu'
|
||||
|
||||
function getInitials(name: string): string {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* ProfileDropdown - User profile menu with sign out
|
||||
*/
|
||||
export function ProfileDropdown() {
|
||||
const { user, signOut } = useAuth()
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await signOut()
|
||||
// TODO: Show sign-in dialog or redirect
|
||||
console.log('Signed out')
|
||||
}
|
||||
|
||||
const initials = user?.name ? getInitials(user.name) : user?.email?.[0]?.toUpperCase() || 'U'
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg border border-border bg-background/95 font-medium text-sm shadow-lg backdrop-blur-md transition-colors hover:bg-accent hover:text-accent-foreground focus:outline-none"
|
||||
type="button"
|
||||
>
|
||||
{initials}
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
{user?.name && (
|
||||
<div className="px-2 py-1.5 text-sm">
|
||||
<div className="font-medium">{user.name}</div>
|
||||
{user.email && <div className="text-muted-foreground text-xs">{user.email}</div>}
|
||||
</div>
|
||||
)}
|
||||
{user?.name && <DropdownMenuItem className="h-px bg-border" />}
|
||||
<DropdownMenuItem className="cursor-pointer" variant="destructive" onClick={handleSignOut}>
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
109
apps/editor/features/community/components/property-dropdown.tsx
Normal file
109
apps/editor/features/community/components/property-dropdown.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
'use client'
|
||||
|
||||
import { Check, ChevronDown, Home, Plus } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { usePropertyStore } from '../lib/properties/store'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/primitives/dropdown-menu'
|
||||
import { NewPropertyDialog } from './new-property-dialog'
|
||||
import { usePropertyScene } from '../lib/models/hooks'
|
||||
|
||||
/**
|
||||
* PropertyDropdown - Shows active property and allows switching between properties
|
||||
*/
|
||||
export function PropertyDropdown() {
|
||||
usePropertyScene() // Load and auto-save property scenes
|
||||
|
||||
// Use property store
|
||||
const properties = usePropertyStore(state => state.properties)
|
||||
const activeProperty = usePropertyStore(state => state.activeProperty)
|
||||
const isLoading = usePropertyStore(state => state.isLoading)
|
||||
const setActiveProperty = usePropertyStore(state => state.setActiveProperty)
|
||||
const fetchProperties = usePropertyStore(state => state.fetchProperties)
|
||||
|
||||
const [isNewPropertyDialogOpen, setIsNewPropertyDialogOpen] = useState(false)
|
||||
|
||||
const handlePropertySelect = async (propertyId: string) => {
|
||||
await setActiveProperty(propertyId)
|
||||
}
|
||||
|
||||
const handleAddNew = () => {
|
||||
setIsNewPropertyDialogOpen(true)
|
||||
}
|
||||
|
||||
const handlePropertyCreated = async (propertyId: string) => {
|
||||
// Set the newly created property as active (this will also fetch properties)
|
||||
await setActiveProperty(propertyId)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="flex h-9 items-center gap-2 rounded-lg border border-border bg-background/95 px-3 text-sm shadow-lg backdrop-blur-md transition-colors hover:bg-accent hover:text-accent-foreground disabled:opacity-50 focus:outline-none"
|
||||
disabled={isLoading}
|
||||
type="button"
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
<span className="max-w-[150px] truncate">
|
||||
{activeProperty
|
||||
? activeProperty.name
|
||||
: properties.length > 0
|
||||
? 'Select Property'
|
||||
: 'Add Property'}
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3 opacity-50" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[280px]">
|
||||
{/* Property list */}
|
||||
{properties.length > 0 ? (
|
||||
<div className="max-h-[300px] overflow-y-auto">
|
||||
{properties.map((property) => (
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
'cursor-pointer text-sm',
|
||||
activeProperty?.id === property.id && 'cursor-default bg-accent',
|
||||
)}
|
||||
key={property.id}
|
||||
onClick={() =>
|
||||
activeProperty?.id === property.id ? null : handlePropertySelect(property.id)
|
||||
}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex-1 truncate font-medium">{property.name}</div>
|
||||
{activeProperty?.id === property.id && (
|
||||
<Check className="h-4 w-4 shrink-0 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-2 py-3 text-center text-muted-foreground text-sm">
|
||||
No properties yet
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add new property option */}
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={handleAddNew}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<span>Add new property</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<NewPropertyDialog
|
||||
open={isNewPropertyDialogOpen}
|
||||
onOpenChange={setIsNewPropertyDialogOpen}
|
||||
onSuccess={handlePropertyCreated}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
148
apps/editor/features/community/components/sign-in-dialog.tsx
Normal file
148
apps/editor/features/community/components/sign-in-dialog.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import { Mail, X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { authClient } from '../lib/auth/client'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/primitives/dialog'
|
||||
|
||||
interface SignInDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* SignInDialog - Magic link authentication dialog
|
||||
*/
|
||||
export function SignInDialog({ open, onOpenChange }: SignInDialogProps) {
|
||||
const [email, setEmail] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
// Use better-auth's magic link sign in
|
||||
const result = await authClient.signIn.magicLink({
|
||||
email,
|
||||
callbackURL: window.location.origin,
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error.message || 'Failed to send magic link')
|
||||
} else {
|
||||
setSuccess(true)
|
||||
setEmail('')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unexpected error occurred')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
if (!isLoading) {
|
||||
onOpenChange(false)
|
||||
// Reset state after a short delay to avoid flash
|
||||
setTimeout(() => {
|
||||
setEmail('')
|
||||
setError(null)
|
||||
setSuccess(false)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Sign in to Pascal</DialogTitle>
|
||||
<button
|
||||
className="absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 disabled:pointer-events-none"
|
||||
disabled={isLoading}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</button>
|
||||
</DialogHeader>
|
||||
|
||||
{success ? (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/20">
|
||||
<Mail className="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-lg">Check your email</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
We've sent a magic link to <strong>{email}</strong>
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Click the link in the email to sign in to your account.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="w-full rounded-md border border-input px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-sm" htmlFor="email">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
autoComplete="email"
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={isLoading}
|
||||
id="email"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="flex w-full items-center justify-center gap-2 rounded-md bg-primary px-4 py-2 text-primary-foreground text-sm transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
disabled={isLoading || !email}
|
||||
type="submit"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent" />
|
||||
Sending magic link...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Mail className="h-4 w-4" />
|
||||
Send magic link
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<p className="text-center text-muted-foreground text-xs">
|
||||
We'll send you a magic link to sign in without a password.
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
7
apps/editor/features/community/lib/auth/client.ts
Normal file
7
apps/editor/features/community/lib/auth/client.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Auth client for the editor using better-auth
|
||||
* Re-exports from @pascal-app/auth package
|
||||
*/
|
||||
|
||||
export { authClient } from '@pascal-app/auth/client'
|
||||
export type { AuthState, User, Session } from '@pascal-app/auth/client'
|
||||
20
apps/editor/features/community/lib/auth/hooks.ts
Normal file
20
apps/editor/features/community/lib/auth/hooks.ts
Normal file
@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { authClient } from './client'
|
||||
|
||||
/**
|
||||
* Hook to access authentication state using better-auth
|
||||
* @returns Current auth state including user, session, and loading status
|
||||
*/
|
||||
export function useAuth() {
|
||||
const session = authClient.useSession()
|
||||
|
||||
return {
|
||||
user: session.data?.user ?? null,
|
||||
session: session.data?.session ?? null,
|
||||
isAuthenticated: !!session.data?.user && !!session.data?.session,
|
||||
isLoading: session.isPending,
|
||||
signOut: () => authClient.signOut(),
|
||||
signIn: authClient.signIn,
|
||||
}
|
||||
}
|
||||
48
apps/editor/features/community/lib/auth/server.ts
Normal file
48
apps/editor/features/community/lib/auth/server.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { headers as nextHeaders } from 'next/headers'
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'
|
||||
|
||||
/**
|
||||
* Get the current session from Better Auth backend (server-side)
|
||||
*/
|
||||
export async function getSession() {
|
||||
try {
|
||||
const headersList = await nextHeaders()
|
||||
|
||||
// Make authenticated request to the auth backend to get session
|
||||
const response = await fetch(`${API_URL}/api/auth/get-session`, {
|
||||
headers: {
|
||||
cookie: headersList.get('cookie') || '',
|
||||
},
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Better Auth returns the session data directly
|
||||
if (data?.user && data?.session) {
|
||||
return {
|
||||
user: data.user,
|
||||
session: data.session,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
console.error('Failed to get session:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user from the session
|
||||
*/
|
||||
export async function getUser() {
|
||||
const session = await getSession()
|
||||
return session?.user ?? null
|
||||
}
|
||||
14
apps/editor/features/community/lib/database/server.ts
Normal file
14
apps/editor/features/community/lib/database/server.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Supabase server client for database access
|
||||
*/
|
||||
|
||||
import { supabaseAdmin } from '@/lib/supabase/server'
|
||||
|
||||
/**
|
||||
* Create a Supabase client for server-side use with service role key
|
||||
* This bypasses RLS and allows server actions to query the database directly
|
||||
* Authentication is handled by Better Auth, permissions enforced by filtering on user_id
|
||||
*/
|
||||
export async function createServerSupabaseClient() {
|
||||
return supabaseAdmin
|
||||
}
|
||||
242
apps/editor/features/community/lib/models/actions.ts
Normal file
242
apps/editor/features/community/lib/models/actions.ts
Normal file
@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Property model actions - Server actions for scene loading/saving
|
||||
* Manages 3D models (scene graphs) stored in properties_models table
|
||||
*/
|
||||
|
||||
'use server'
|
||||
|
||||
import type { AnyNode, AnyNodeId } from '@pascal-app/core'
|
||||
import { createServerSupabaseClient } from '../database/server'
|
||||
import { getSession } from '../auth/server'
|
||||
import { createId } from '../utils/id-generator'
|
||||
import type { ActionResult } from '../properties/actions'
|
||||
|
||||
export interface SceneGraph {
|
||||
nodes: Record<AnyNodeId, AnyNode>
|
||||
rootNodeIds: AnyNodeId[]
|
||||
}
|
||||
|
||||
export interface PropertyModel {
|
||||
id: string
|
||||
name: string
|
||||
version: number
|
||||
draft: boolean
|
||||
property_id: string
|
||||
scene_graph: SceneGraph | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest model for a property (highest version)
|
||||
*/
|
||||
export async function getPropertyModel(propertyId: string): Promise<ActionResult<PropertyModel | null>> {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authenticated',
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
// Get the property to verify ownership
|
||||
const { data: property, error: propertyError } = await supabase
|
||||
.from('properties')
|
||||
.select('id, owner_id')
|
||||
.eq('id', propertyId)
|
||||
.single<{ id: string; owner_id: string }>()
|
||||
|
||||
if (propertyError || !property) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Property not found',
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if (property.owner_id !== session.user.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
// Get the latest model (highest version, then most recent)
|
||||
const { data: model, error: modelError } = await supabase
|
||||
.from('properties_models')
|
||||
.select('*')
|
||||
.eq('property_id', propertyId)
|
||||
.is('deleted_at', null)
|
||||
.order('version', { ascending: false })
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single<PropertyModel>()
|
||||
|
||||
console.log('[getPropertyModel] Query result:', {
|
||||
propertyId,
|
||||
hasModel: !!model,
|
||||
modelError: modelError?.message,
|
||||
errorCode: modelError?.code,
|
||||
modelKeys: model ? Object.keys(model) : [],
|
||||
})
|
||||
|
||||
if (modelError) {
|
||||
// No model found is not an error - just return null
|
||||
if (modelError.code === 'PGRST116') {
|
||||
console.log('[getPropertyModel] No model found (PGRST116), returning null')
|
||||
return {
|
||||
success: true,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[getPropertyModel] Database error:', modelError)
|
||||
return {
|
||||
success: false,
|
||||
error: modelError.message,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[getPropertyModel] Model found:', {
|
||||
id: model.id,
|
||||
version: model.version,
|
||||
hasSceneGraph: !!model.scene_graph,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: model as PropertyModel,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch property model',
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update a property model's scene graph
|
||||
* If a model exists, updates it. Otherwise creates a new one.
|
||||
*/
|
||||
export async function savePropertyModel(
|
||||
propertyId: string,
|
||||
sceneGraph: SceneGraph,
|
||||
): Promise<ActionResult<PropertyModel>> {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authenticated',
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
// Get the property to verify ownership
|
||||
const { data: property, error: propertyError } = await supabase
|
||||
.from('properties')
|
||||
.select('id, owner_id, name')
|
||||
.eq('id', propertyId)
|
||||
.single<{ id: string; owner_id: string; name: string }>()
|
||||
|
||||
if (propertyError || !property) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Property not found',
|
||||
}
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if (property.owner_id !== session.user.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a model already exists
|
||||
const { data: existingModel } = await supabase
|
||||
.from('properties_models')
|
||||
.select('id, version')
|
||||
.eq('property_id', propertyId)
|
||||
.is('deleted_at', null)
|
||||
.order('version', { ascending: false })
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single<{ id: string; version: number }>()
|
||||
|
||||
if (existingModel) {
|
||||
// Update existing model
|
||||
const updateData = {
|
||||
scene_graph: sceneGraph,
|
||||
updated_at: new Date().toISOString(),
|
||||
}
|
||||
const { data: updatedModel, error: updateError } = (await (supabase
|
||||
.from('properties_models') as any)
|
||||
.update(updateData)
|
||||
.eq('id', existingModel.id)
|
||||
.select()
|
||||
.single()) as { data: PropertyModel | null; error: any }
|
||||
|
||||
if (updateError) {
|
||||
return {
|
||||
success: false,
|
||||
error: updateError.message,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedModel as PropertyModel,
|
||||
message: 'Model updated successfully',
|
||||
}
|
||||
} else {
|
||||
// Create new model
|
||||
const modelId = createId('model')
|
||||
|
||||
const insertData = {
|
||||
id: modelId,
|
||||
property_id: propertyId,
|
||||
name: `${property.name} - Editor`,
|
||||
version: 1,
|
||||
draft: true,
|
||||
scene_graph: sceneGraph,
|
||||
}
|
||||
const { data: newModel, error: createError } = (await (supabase
|
||||
.from('properties_models') as any)
|
||||
.insert(insertData)
|
||||
.select()
|
||||
.single()) as { data: PropertyModel | null; error: any }
|
||||
|
||||
if (createError) {
|
||||
return {
|
||||
success: false,
|
||||
error: createError.message,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: newModel as PropertyModel,
|
||||
message: 'Model created successfully',
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to save property model',
|
||||
}
|
||||
}
|
||||
}
|
||||
142
apps/editor/features/community/lib/models/hooks.ts
Normal file
142
apps/editor/features/community/lib/models/hooks.ts
Normal file
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Hooks for property model (scene) loading and auto-saving
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import useEditor from '@/store/use-editor'
|
||||
import { usePropertyStore } from '../properties/store'
|
||||
import { getPropertyModel, savePropertyModel } from './actions'
|
||||
|
||||
/**
|
||||
* Load the scene when a property becomes active
|
||||
* Saves changes automatically with debouncing
|
||||
*/
|
||||
export function usePropertyScene() {
|
||||
// Subscribe to property store
|
||||
const activeProperty = usePropertyStore((state) => state.activeProperty)
|
||||
const isLoadingProperty = usePropertyStore((state) => state.isLoading)
|
||||
|
||||
const lastPropertyIdRef = useRef<string | null>(null)
|
||||
const saveTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
||||
const isSavingRef = useRef(false)
|
||||
const currentPropertyIdRef = useRef<string | null>(null)
|
||||
|
||||
// Extract property ID for dependency tracking
|
||||
const propertyId = activeProperty?.id ?? null
|
||||
const propertyName = activeProperty?.name ?? null
|
||||
|
||||
// Load scene when active property changes
|
||||
useEffect(() => {
|
||||
if (isLoadingProperty) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!propertyId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if same property
|
||||
if (lastPropertyIdRef.current === propertyId) {
|
||||
return
|
||||
}
|
||||
|
||||
lastPropertyIdRef.current = propertyId
|
||||
|
||||
// Load the property's scene
|
||||
async function loadScene() {
|
||||
try {
|
||||
const result = await getPropertyModel(propertyId || '')
|
||||
|
||||
if (result.success && result.data?.scene_graph) {
|
||||
// Load the scene graph into the store
|
||||
const { nodes, rootNodeIds } = result.data.scene_graph
|
||||
useScene.getState().setScene(nodes, rootNodeIds)
|
||||
} else {
|
||||
// No scene found - clear the scene
|
||||
useScene.getState().clearScene()
|
||||
}
|
||||
} catch (error) {
|
||||
// Fall back to clear scene
|
||||
useScene.getState().clearScene()
|
||||
}
|
||||
|
||||
// Reset editor state after loading/clearing scene
|
||||
useEditor.getState().setPhase('site')
|
||||
useViewer.getState().setSelection({
|
||||
buildingId: null,
|
||||
levelId: null,
|
||||
selectedIds: [],
|
||||
zoneId: null,
|
||||
})
|
||||
}
|
||||
|
||||
loadScene()
|
||||
}, [propertyId, isLoadingProperty])
|
||||
|
||||
// Auto-save scene changes with debouncing
|
||||
useEffect(() => {
|
||||
if (!propertyId) {
|
||||
currentPropertyIdRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
currentPropertyIdRef.current = propertyId
|
||||
|
||||
// Subscribe to any scene changes
|
||||
// Use JSON stringification to detect any node changes, not just count
|
||||
let lastNodesSnapshot = JSON.stringify(useScene.getState().nodes)
|
||||
|
||||
const unsubscribe = useScene.subscribe((state) => {
|
||||
const currentNodesSnapshot = JSON.stringify(state.nodes)
|
||||
|
||||
// Only trigger save if nodes actually changed
|
||||
if (currentNodesSnapshot === lastNodesSnapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
lastNodesSnapshot = currentNodesSnapshot
|
||||
const nodes = state.nodes
|
||||
|
||||
// Skip if currently saving
|
||||
if (isSavingRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear existing timeout
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
}
|
||||
|
||||
// Debounce save by 2 seconds
|
||||
saveTimeoutRef.current = setTimeout(async () => {
|
||||
// Get the current property ID at save time (not the captured value)
|
||||
const currentPropertyId = currentPropertyIdRef.current
|
||||
if (!currentPropertyId) {
|
||||
return
|
||||
}
|
||||
|
||||
const rootNodeIds = useScene.getState().rootNodeIds
|
||||
const sceneGraph = { nodes, rootNodeIds }
|
||||
|
||||
isSavingRef.current = true
|
||||
|
||||
try {
|
||||
await savePropertyModel(currentPropertyId, sceneGraph)
|
||||
} finally {
|
||||
isSavingRef.current = false
|
||||
}
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (saveTimeoutRef.current) {
|
||||
clearTimeout(saveTimeoutRef.current)
|
||||
}
|
||||
unsubscribe()
|
||||
}
|
||||
}, [propertyId])
|
||||
}
|
||||
328
apps/editor/features/community/lib/properties/actions.ts
Normal file
328
apps/editor/features/community/lib/properties/actions.ts
Normal file
@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Property actions - Server actions for property management
|
||||
* Uses Better Auth session + Supabase to query the same database as the monorepo
|
||||
*/
|
||||
|
||||
'use server'
|
||||
|
||||
import { createServerSupabaseClient } from '../database/server'
|
||||
import { getSession } from '../auth/server'
|
||||
import { createId } from '../utils/id-generator'
|
||||
import type { CreatePropertyParams, Property } from './types'
|
||||
|
||||
export type ActionResult<T = unknown> = {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all properties for the current user
|
||||
*/
|
||||
export async function getUserProperties(): Promise<ActionResult<Property[]>> {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authenticated',
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
// Query properties table with address relation
|
||||
const { data, error } = await supabase
|
||||
.from('properties')
|
||||
.select(`
|
||||
*,
|
||||
address:properties_addresses(*)
|
||||
`)
|
||||
.eq('owner_id', session.user.id)
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data as Property[],
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch properties',
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active property for the current session
|
||||
*/
|
||||
export async function getActiveProperty(): Promise<ActionResult<Property | null>> {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authenticated',
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
// Get session's active_property_id from sessions table
|
||||
const { data: sessionData, error: sessionError } = await supabase
|
||||
.from('auth_sessions')
|
||||
.select('active_property_id')
|
||||
.eq('user_id', session.user.id)
|
||||
.single<{ active_property_id: string | null }>()
|
||||
|
||||
if (sessionError || !sessionData?.active_property_id) {
|
||||
return {
|
||||
success: true,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
// Get the property with address
|
||||
const { data, error } = await supabase
|
||||
.from('properties')
|
||||
.select(`
|
||||
*,
|
||||
address:properties_addresses(*)
|
||||
`)
|
||||
.eq('id', sessionData.active_property_id)
|
||||
.single<Property>()
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data as Property,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch active property',
|
||||
data: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active property for the current session
|
||||
*/
|
||||
export async function setActiveProperty(propertyId: string | null): Promise<ActionResult> {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authenticated',
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
// Update session's active_property_id
|
||||
const { error } = await (supabase
|
||||
.from('auth_sessions') as any)
|
||||
.update({ active_property_id: propertyId })
|
||||
.eq('user_id', session.user.id)
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: propertyId ? 'Active property updated' : 'Active property cleared',
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to set active property',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new property
|
||||
*/
|
||||
export async function createProperty(params: CreatePropertyParams): Promise<ActionResult<Property>> {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authenticated',
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
// Generate IDs for address and property
|
||||
const addressId = createId('address')
|
||||
const propertyId = createId('property')
|
||||
|
||||
// First, create the address
|
||||
const addressData = {
|
||||
id: addressId,
|
||||
street_number: params.streetNumber,
|
||||
route: params.route,
|
||||
city: params.city || '',
|
||||
state: params.state || '',
|
||||
postal_code: params.postalCode || '',
|
||||
country: params.country || 'US',
|
||||
latitude: params.center[1].toString(),
|
||||
longitude: params.center[0].toString(),
|
||||
}
|
||||
const { data: address, error: addressError } = (await (supabase
|
||||
.from('properties_addresses') as any)
|
||||
.insert(addressData)
|
||||
.select()
|
||||
.single()) as { data: Property['address'] | null; error: any }
|
||||
|
||||
if (addressError || !address) {
|
||||
return {
|
||||
success: false,
|
||||
error: addressError?.message || 'Failed to create address',
|
||||
}
|
||||
}
|
||||
|
||||
// Create the property
|
||||
const propertyData = {
|
||||
id: propertyId,
|
||||
name: params.name,
|
||||
address_id: address.id,
|
||||
owner_id: session.user.id,
|
||||
details_json: {
|
||||
coordinates: params.center,
|
||||
createdFrom: 'editor-app',
|
||||
},
|
||||
}
|
||||
const { data, error } = (await (supabase
|
||||
.from('properties') as any)
|
||||
.insert(propertyData)
|
||||
.select(`
|
||||
*,
|
||||
address:properties_addresses(*)
|
||||
`)
|
||||
.single()) as { data: Property | null; error: any }
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data as Property,
|
||||
message: 'Property created successfully',
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create property',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a property with the given address already exists
|
||||
*/
|
||||
export async function checkPropertyDuplicate(params: {
|
||||
streetNumber?: string
|
||||
route?: string
|
||||
city?: string
|
||||
state?: string
|
||||
postalCode?: string
|
||||
}): Promise<
|
||||
ActionResult<{
|
||||
isDuplicate: boolean
|
||||
isUserProperty?: boolean
|
||||
existingProperty?: Property
|
||||
}>
|
||||
> {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authenticated',
|
||||
}
|
||||
}
|
||||
|
||||
const supabase = await createServerSupabaseClient()
|
||||
|
||||
// Query for existing property with matching address
|
||||
const { data, error } = await supabase
|
||||
.from('properties')
|
||||
.select(`
|
||||
*,
|
||||
address:properties_addresses!inner(*)
|
||||
`)
|
||||
.eq('address.street_number', params.streetNumber || '')
|
||||
.eq('address.route', params.route || '')
|
||||
.eq('address.city', params.city || '')
|
||||
.eq('address.state', params.state || '')
|
||||
.eq('address.postal_code', params.postalCode || '')
|
||||
.limit(1)
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
}
|
||||
}
|
||||
|
||||
if (data && data.length > 0) {
|
||||
const existingProperty = data[0] as unknown as Property
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
isDuplicate: true,
|
||||
isUserProperty: existingProperty.owner_id === session.user.id,
|
||||
existingProperty,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
isDuplicate: false,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to check for duplicates',
|
||||
}
|
||||
}
|
||||
}
|
||||
234
apps/editor/features/community/lib/properties/hooks.ts
Normal file
234
apps/editor/features/community/lib/properties/hooks.ts
Normal file
@ -0,0 +1,234 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
getActiveProperty,
|
||||
getUserProperties,
|
||||
setActiveProperty as setActivePropertyAction,
|
||||
} from './actions'
|
||||
import type { Property } from './types'
|
||||
|
||||
interface UsePropertiesReturn {
|
||||
properties: Property[]
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
refetch: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and manage user properties
|
||||
*/
|
||||
export function useProperties(): UsePropertiesReturn {
|
||||
const [properties, setProperties] = useState<Property[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchProperties = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
const result = await getUserProperties()
|
||||
|
||||
if (result.success) {
|
||||
setProperties(result.data || [])
|
||||
} else {
|
||||
setError(result.error || 'Failed to fetch properties')
|
||||
setProperties([])
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unexpected error occurred')
|
||||
setProperties([])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchProperties()
|
||||
}, [fetchProperties])
|
||||
|
||||
return {
|
||||
properties,
|
||||
isLoading,
|
||||
error,
|
||||
refetch: fetchProperties,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch a single property by ID
|
||||
*/
|
||||
export function useProperty(propertyId: string | undefined) {
|
||||
const [property, setProperty] = useState<Property | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!propertyId) {
|
||||
setProperty(null)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const fetchProperty = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
const result = await getUserProperties()
|
||||
|
||||
if (result.success) {
|
||||
const found = result.data?.find((p) => p.id === propertyId)
|
||||
if (found) {
|
||||
setProperty(found)
|
||||
} else {
|
||||
setError('Property not found')
|
||||
setProperty(null)
|
||||
}
|
||||
} else {
|
||||
setError(result.error || 'Failed to fetch property')
|
||||
setProperty(null)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unexpected error occurred')
|
||||
setProperty(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchProperty()
|
||||
}, [propertyId])
|
||||
|
||||
return {
|
||||
property,
|
||||
isLoading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage the active property for the current session
|
||||
*/
|
||||
export function useActiveProperty() {
|
||||
const [activeProperty, setActivePropertyState] = useState<Property | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const isInitialFetchRef = useRef(true)
|
||||
|
||||
const fetchActiveProperty = useCallback(async (allowAutoSelect = false) => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
const result = await getActiveProperty()
|
||||
|
||||
if (result.success) {
|
||||
setActivePropertyState(result.data || null)
|
||||
|
||||
// If no active property is set, automatically set the first property as active
|
||||
// Only do this on initial mount to avoid interfering with property selection/creation
|
||||
if (!result.data && allowAutoSelect) {
|
||||
console.log('[useActiveProperty] No active property, checking if we should auto-select')
|
||||
const propertiesResult = await getUserProperties()
|
||||
|
||||
if (
|
||||
propertiesResult.success &&
|
||||
propertiesResult.data &&
|
||||
propertiesResult.data.length > 0
|
||||
) {
|
||||
console.log('[useActiveProperty] Found properties, auto-selecting first one')
|
||||
const firstProperty = propertiesResult.data[0]
|
||||
if (firstProperty) {
|
||||
const setActiveResult = await setActivePropertyAction(firstProperty.id)
|
||||
|
||||
if (setActiveResult.success) {
|
||||
console.log('[useActiveProperty] Auto-selected property:', firstProperty.name)
|
||||
setActivePropertyState(firstProperty)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!result.data && !allowAutoSelect) {
|
||||
console.log('[useActiveProperty] No active property but auto-select is disabled')
|
||||
}
|
||||
} else {
|
||||
setError(result.error || 'Failed to fetch active property')
|
||||
setActivePropertyState(null)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unexpected error occurred')
|
||||
setActivePropertyState(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const changeActiveProperty = useCallback(
|
||||
async (propertyId: string | null) => {
|
||||
try {
|
||||
setIsPending(true)
|
||||
setIsLoading(true)
|
||||
const result = await setActivePropertyAction(propertyId)
|
||||
|
||||
if (result.success) {
|
||||
if (propertyId) {
|
||||
// Fetch the property data and set it immediately
|
||||
console.log('[useActiveProperty] Fetching property data for ID:', propertyId)
|
||||
const propertiesResult = await getUserProperties()
|
||||
|
||||
if (propertiesResult.success && propertiesResult.data) {
|
||||
const selectedProperty = propertiesResult.data.find(p => p.id === propertyId)
|
||||
if (selectedProperty) {
|
||||
console.log('[useActiveProperty] Found property, setting as active:', selectedProperty.name)
|
||||
console.log('[useActiveProperty] Current isLoading state:', isLoading)
|
||||
setActivePropertyState(selectedProperty)
|
||||
setIsLoading(false)
|
||||
console.log('[useActiveProperty] Set isLoading to false')
|
||||
} else {
|
||||
console.error('[useActiveProperty] Property not found in user properties')
|
||||
// Fall back to refetch
|
||||
await fetchActiveProperty(false)
|
||||
}
|
||||
} else {
|
||||
console.error('[useActiveProperty] Failed to fetch properties')
|
||||
// Fall back to refetch
|
||||
await fetchActiveProperty(false)
|
||||
}
|
||||
} else {
|
||||
setActivePropertyState(null)
|
||||
setIsLoading(false)
|
||||
}
|
||||
} else {
|
||||
console.error(result.error || 'Failed to update active property')
|
||||
setIsLoading(false)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err instanceof Error ? err.message : 'An unexpected error occurred')
|
||||
setIsLoading(false)
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
},
|
||||
[fetchActiveProperty],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Only allow auto-select on the initial mount
|
||||
const allowAutoSelect = isInitialFetchRef.current
|
||||
if (isInitialFetchRef.current) {
|
||||
isInitialFetchRef.current = false
|
||||
}
|
||||
fetchActiveProperty(allowAutoSelect)
|
||||
}, [fetchActiveProperty])
|
||||
|
||||
return {
|
||||
activeProperty,
|
||||
isLoading,
|
||||
error,
|
||||
setActiveProperty: changeActiveProperty,
|
||||
isPending,
|
||||
refetch: fetchActiveProperty,
|
||||
}
|
||||
}
|
||||
126
apps/editor/features/community/lib/properties/store.ts
Normal file
126
apps/editor/features/community/lib/properties/store.ts
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Property store - Zustand store for property state management
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import type { Property } from './types'
|
||||
import {
|
||||
getActiveProperty,
|
||||
getUserProperties,
|
||||
setActiveProperty as setActivePropertyAction,
|
||||
} from './actions'
|
||||
|
||||
interface PropertyStore {
|
||||
// State
|
||||
activeProperty: Property | null
|
||||
properties: Property[]
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
|
||||
// Actions
|
||||
fetchProperties: () => Promise<void>
|
||||
fetchActiveProperty: () => Promise<void>
|
||||
setActiveProperty: (propertyId: string) => Promise<void>
|
||||
initialize: () => Promise<void>
|
||||
}
|
||||
|
||||
export const usePropertyStore = create<PropertyStore>((set, get) => ({
|
||||
// Initial state
|
||||
activeProperty: null,
|
||||
properties: [],
|
||||
isLoading: true,
|
||||
error: null,
|
||||
|
||||
// Fetch all properties
|
||||
fetchProperties: async () => {
|
||||
const result = await getUserProperties()
|
||||
|
||||
if (result.success) {
|
||||
set({ properties: result.data || [], error: null })
|
||||
} else {
|
||||
set({ error: result.error || 'Failed to fetch properties', properties: [] })
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch the active property from database
|
||||
fetchActiveProperty: async () => {
|
||||
set({ isLoading: true })
|
||||
|
||||
const result = await getActiveProperty()
|
||||
|
||||
if (result.success) {
|
||||
set({
|
||||
activeProperty: result.data || null,
|
||||
isLoading: false,
|
||||
error: null
|
||||
})
|
||||
|
||||
// If no active property, auto-select the first one
|
||||
if (!result.data) {
|
||||
const propertiesResult = await getUserProperties()
|
||||
if (propertiesResult.success && propertiesResult.data && propertiesResult.data.length > 0) {
|
||||
const firstProperty = propertiesResult.data[0]
|
||||
if (firstProperty) {
|
||||
await get().setActiveProperty(firstProperty.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
set({
|
||||
error: result.error || 'Failed to fetch active property',
|
||||
activeProperty: null,
|
||||
isLoading: false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Set active property
|
||||
setActiveProperty: async (propertyId: string) => {
|
||||
set({ isLoading: true })
|
||||
|
||||
// Update database
|
||||
const result = await setActivePropertyAction(propertyId)
|
||||
|
||||
if (result.success) {
|
||||
// Fetch properties to get the full property object
|
||||
const propertiesResult = await getUserProperties()
|
||||
|
||||
if (propertiesResult.success && propertiesResult.data) {
|
||||
const selectedProperty = propertiesResult.data.find(p => p.id === propertyId)
|
||||
|
||||
if (selectedProperty) {
|
||||
set({
|
||||
activeProperty: selectedProperty,
|
||||
properties: propertiesResult.data,
|
||||
isLoading: false,
|
||||
error: null
|
||||
})
|
||||
} else {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: 'Property not found'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: 'Failed to fetch properties'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
set({
|
||||
isLoading: false,
|
||||
error: result.error || 'Failed to set active property'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize - fetch both properties and active property
|
||||
initialize: async () => {
|
||||
set({ isLoading: true })
|
||||
await Promise.all([
|
||||
get().fetchProperties(),
|
||||
get().fetchActiveProperty(),
|
||||
])
|
||||
},
|
||||
}))
|
||||
43
apps/editor/features/community/lib/properties/types.ts
Normal file
43
apps/editor/features/community/lib/properties/types.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Property-related type definitions
|
||||
* Isolated from monorepo database schema
|
||||
*/
|
||||
|
||||
export type Property = {
|
||||
id: string
|
||||
name: string
|
||||
owner_id: string
|
||||
organization_id: string | null
|
||||
address_id: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
address: {
|
||||
id: string
|
||||
street_number?: string
|
||||
route?: string
|
||||
city?: string
|
||||
state?: string
|
||||
postal_code?: string
|
||||
country?: string
|
||||
latitude?: string
|
||||
longitude?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type CreatePropertyParams = {
|
||||
name: string
|
||||
center: [number, number]
|
||||
streetNumber?: string
|
||||
route?: string
|
||||
routeShort?: string
|
||||
neighborhood?: string
|
||||
city?: string
|
||||
county?: string
|
||||
state?: string
|
||||
stateLong?: string
|
||||
postalCode?: string
|
||||
postalCodeSuffix?: string
|
||||
country?: string
|
||||
countryLong?: string
|
||||
rawJson?: Record<string, unknown>
|
||||
}
|
||||
13
apps/editor/features/community/lib/utils/id-generator.ts
Normal file
13
apps/editor/features/community/lib/utils/id-generator.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { customAlphabet } from 'nanoid'
|
||||
|
||||
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
const nanoid = customAlphabet(alphabet, 16)
|
||||
|
||||
/**
|
||||
* Generate a unique ID with optional prefix (matches monorepo implementation)
|
||||
* @example createId('user') => 'user_Abc123...'
|
||||
*/
|
||||
export const createId = (prefix?: string) => {
|
||||
const id = nanoid()
|
||||
return prefix ? `${prefix}_${id}` : id
|
||||
}
|
||||
44
apps/editor/lib/auth.ts
Normal file
44
apps/editor/lib/auth.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { createAuth } from '@pascal-app/auth/server'
|
||||
import { db } from '@pascal-app/db'
|
||||
import { Resend } from 'resend'
|
||||
|
||||
const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null
|
||||
|
||||
export const auth = createAuth({
|
||||
db,
|
||||
appName: 'Pascal Editor',
|
||||
baseURL: process.env.BETTER_AUTH_URL!,
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
sendMagicLink: async ({ email, url }) => {
|
||||
if (!resend) {
|
||||
console.log(`[DEV] Magic link for ${email}: ${url}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await resend.emails.send({
|
||||
from: 'Pascal <noreply@pascal.app>',
|
||||
to: email,
|
||||
subject: 'Sign in to Pascal Editor',
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2>Sign in to Pascal Editor</h2>
|
||||
<p>Click the button below to sign in to your account:</p>
|
||||
<a href="${url}" style="display: inline-block; background-color: #000; color: #fff; padding: 12px 24px; text-decoration: none; border-radius: 6px; margin: 20px 0;">
|
||||
Sign In
|
||||
</a>
|
||||
<p style="color: #666; font-size: 14px;">This link will expire in 5 minutes.</p>
|
||||
<p style="color: #666; font-size: 14px;">If you didn't request this email, you can safely ignore it.</p>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
console.log(`✓ Magic link email sent to ${email}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to send magic link email:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export type Session = typeof auth.$Infer.Session
|
||||
export type User = typeof auth.$Infer.Session.user
|
||||
11
apps/editor/lib/supabase/client.ts
Normal file
11
apps/editor/lib/supabase/client.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import type { SupabaseDatabase } from '@pascal-app/db'
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
|
||||
/**
|
||||
* Supabase client for client-side use with anon key
|
||||
* Uses Row Level Security (RLS) policies
|
||||
*/
|
||||
export const supabase = createClient<SupabaseDatabase>(supabaseUrl, supabaseAnonKey)
|
||||
21
apps/editor/lib/supabase/server.ts
Normal file
21
apps/editor/lib/supabase/server.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import type { SupabaseDatabase } from '@pascal-app/db'
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||
const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
|
||||
|
||||
/**
|
||||
* Supabase client for server-side use with service role key
|
||||
* Bypasses Row Level Security (RLS) - use with caution
|
||||
* Always filter by user_id to enforce permissions
|
||||
*/
|
||||
export const supabaseAdmin = createClient<SupabaseDatabase>(
|
||||
supabaseUrl,
|
||||
supabaseServiceRoleKey,
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
autoRefreshToken: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -11,7 +11,9 @@
|
||||
"check-types": "next typegen && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pascal-app/auth": "*",
|
||||
"@pascal-app/core": "*",
|
||||
"@pascal-app/db": "*",
|
||||
"@pascal-app/viewer": "*",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
@ -25,14 +27,18 @@
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-google-maps/api": "^2.20.8",
|
||||
"@react-three/uikit-lucide": "^1.0.60",
|
||||
"@repo/ui": "*",
|
||||
"@supabase/supabase-js": "^2.95.3",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/three": "^0.182.0",
|
||||
"better-auth": "^1.4.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.26.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "16.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.2.0",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
apps/editor/public/audios/radios/classic/Clockwork Tea Party.mp3
Normal file
BIN
apps/editor/public/audios/radios/classic/Clockwork Tea Party.mp3
Normal file
Binary file not shown.
BIN
apps/editor/public/audios/radios/classic/Clockwork Teacups.mp3
Normal file
BIN
apps/editor/public/audios/radios/classic/Clockwork Teacups.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
apps/editor/public/audios/radios/classic/Glass Atrium.mp3
Normal file
BIN
apps/editor/public/audios/radios/classic/Glass Atrium.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
apps/editor/public/audios/sfx/grid_snap.mp3
Normal file
BIN
apps/editor/public/audios/sfx/grid_snap.mp3
Normal file
Binary file not shown.
BIN
apps/editor/public/audios/sfx/item_delete.mp3
Normal file
BIN
apps/editor/public/audios/sfx/item_delete.mp3
Normal file
Binary file not shown.
BIN
apps/editor/public/audios/sfx/item_pick.mp3
Normal file
BIN
apps/editor/public/audios/sfx/item_pick.mp3
Normal file
Binary file not shown.
BIN
apps/editor/public/audios/sfx/item_place.mp3
Normal file
BIN
apps/editor/public/audios/sfx/item_place.mp3
Normal file
Binary file not shown.
BIN
apps/editor/public/audios/sfx/structure_build.mp3
Normal file
BIN
apps/editor/public/audios/sfx/structure_build.mp3
Normal file
Binary file not shown.
BIN
apps/editor/public/audios/sfx/structure_delete.mp3
Normal file
BIN
apps/editor/public/audios/sfx/structure_delete.mp3
Normal file
Binary file not shown.
351
bun.lock
351
bun.lock
@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "editor",
|
||||
@ -13,6 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"supabase": "2.75.3",
|
||||
"turbo": "^2.8.1",
|
||||
"typescript": "5.9.2",
|
||||
"ultracite": "^7.1.1",
|
||||
@ -22,7 +24,9 @@
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@pascal-app/auth": "*",
|
||||
"@pascal-app/core": "*",
|
||||
"@pascal-app/db": "*",
|
||||
"@pascal-app/viewer": "*",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
@ -36,14 +40,18 @@
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-google-maps/api": "^2.20.8",
|
||||
"@react-three/uikit-lucide": "^1.0.60",
|
||||
"@repo/ui": "*",
|
||||
"@supabase/supabase-js": "^2.95.3",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/three": "^0.182.0",
|
||||
"better-auth": "^1.4.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.26.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "16.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.2.0",
|
||||
@ -60,6 +68,19 @@
|
||||
"typescript": "5.9.2",
|
||||
},
|
||||
},
|
||||
"packages/auth": {
|
||||
"name": "@pascal-app/auth",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@pascal-app/db": "*",
|
||||
"better-auth": "^1.4.18",
|
||||
"resend": "^4.0.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/typescript-config": "*",
|
||||
"typescript": "5.9.2",
|
||||
},
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@pascal-app/core",
|
||||
"version": "0.1.11",
|
||||
@ -87,6 +108,22 @@
|
||||
"three": "^0.182",
|
||||
},
|
||||
},
|
||||
"packages/db": {
|
||||
"name": "@pascal-app/db",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.95.3",
|
||||
"drizzle-orm": "^0.39.0",
|
||||
"drizzle-zod": "^0.5.1",
|
||||
"nanoid": "^5.0.9",
|
||||
"postgres": "^3.4.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/typescript-config": "*",
|
||||
"drizzle-kit": "^0.30.0",
|
||||
"typescript": "5.9.2",
|
||||
},
|
||||
},
|
||||
"packages/eslint-config": {
|
||||
"name": "@repo/eslint-config",
|
||||
"version": "0.0.0",
|
||||
@ -151,6 +188,14 @@
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
|
||||
"@better-auth/core": ["@better-auth/core@1.4.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg=="],
|
||||
|
||||
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.18", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.18" } }, "sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ=="],
|
||||
|
||||
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
||||
|
||||
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.3.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.13", "@biomejs/cli-darwin-x64": "2.3.13", "@biomejs/cli-linux-arm64": "2.3.13", "@biomejs/cli-linux-arm64-musl": "2.3.13", "@biomejs/cli-linux-x64": "2.3.13", "@biomejs/cli-linux-x64-musl": "2.3.13", "@biomejs/cli-win32-arm64": "2.3.13", "@biomejs/cli-win32-x64": "2.3.13" }, "bin": { "biome": "bin/biome" } }, "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="],
|
||||
@ -175,8 +220,60 @@
|
||||
|
||||
"@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
|
||||
|
||||
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
@ -203,6 +300,10 @@
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||
|
||||
"@googlemaps/js-api-loader": ["@googlemaps/js-api-loader@1.16.8", "", {}, "sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ=="],
|
||||
|
||||
"@googlemaps/markerclusterer": ["@googlemaps/markerclusterer@2.5.3", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "supercluster": "^8.0.1" } }, "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||
@ -265,6 +366,8 @@
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
||||
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
@ -299,16 +402,26 @@
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Tr0j94MphimCCks+1rtYPzQFK+faJuhHWCegU9S9gDlgyOk8Y3kPmO64UcjyzZAlligeBtYZ/2bEyrKq0d2wqQ=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@pascal-app/auth": ["@pascal-app/auth@workspace:packages/auth"],
|
||||
|
||||
"@pascal-app/core": ["@pascal-app/core@workspace:packages/core"],
|
||||
|
||||
"@pascal-app/db": ["@pascal-app/db@workspace:packages/db"],
|
||||
|
||||
"@pascal-app/viewer": ["@pascal-app/viewer@workspace:packages/viewer"],
|
||||
|
||||
"@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
|
||||
|
||||
"@pmndrs/msdfonts": ["@pmndrs/msdfonts@1.0.61", "", {}, "sha512-Nxk+3dAdsXFdx5pq1IHNe1j4zxogkAfhmBKUIHS+aJUs45q/XMtlADVrHLooiQ/Amlp9tXAvbvFYUOGd897POw=="],
|
||||
|
||||
"@pmndrs/uikit": ["@pmndrs/uikit@1.0.61", "", { "dependencies": { "@pmndrs/msdfonts": "^1.0.61", "@pmndrs/uikit-pub-sub": "^1.0.61", "@preact/signals-core": "^1.5.1", "@zappar/msdf-generator": "^1.2.4", "yoga-layout": "^3.2.1" }, "peerDependencies": { "three": ">=0.162" } }, "sha512-xoNp2jKRoHa7INC3QaAL2ddpGmKy+eORMh4zoNds17EndqyBrqYzfWJK+PVmei/5MQNBTi0KrY2qxBGj6hB0cg=="],
|
||||
@ -397,6 +510,14 @@
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||
|
||||
"@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="],
|
||||
|
||||
"@react-google-maps/api": ["@react-google-maps/api@2.20.8", "", { "dependencies": { "@googlemaps/js-api-loader": "1.16.8", "@googlemaps/markerclusterer": "2.5.3", "@react-google-maps/infobox": "2.20.0", "@react-google-maps/marker-clusterer": "2.20.0", "@types/google.maps": "3.58.1", "invariant": "2.2.4" }, "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19", "react-dom": "^16.8 || ^17 || ^18 || ^19" } }, "sha512-wtLYFtCGXK3qbIz1H5to3JxbosPnKsvjDKhqGylXUb859EskhzR7OpuNt0LqdLarXUtZCJTKzPn3BNaekNIahg=="],
|
||||
|
||||
"@react-google-maps/infobox": ["@react-google-maps/infobox@2.20.0", "", {}, "sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ=="],
|
||||
|
||||
"@react-google-maps/marker-clusterer": ["@react-google-maps/marker-clusterer@2.20.0", "", {}, "sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw=="],
|
||||
|
||||
"@react-three/drei": ["@react-three/drei@10.7.7", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@mediapipe/tasks-vision": "0.10.17", "@monogrid/gainmap-js": "^3.0.6", "@use-gesture/react": "^10.3.1", "camera-controls": "^3.1.0", "cross-env": "^7.0.3", "detect-gpu": "^5.0.56", "glsl-noise": "^0.0.0", "hls.js": "^1.5.17", "maath": "^0.10.8", "meshline": "^3.3.1", "stats-gl": "^2.2.8", "stats.js": "^0.17.0", "suspend-react": "^0.1.3", "three-mesh-bvh": "^0.8.3", "three-stdlib": "^2.35.6", "troika-three-text": "^0.52.4", "tunnel-rat": "^0.1.2", "use-sync-external-store": "^1.4.0", "utility-types": "^3.11.0", "zustand": "^5.0.1" }, "peerDependencies": { "@react-three/fiber": "^9.0.0", "react": "^19", "react-dom": "^19", "three": ">=0.159" }, "optionalPeers": ["react-dom"] }, "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ=="],
|
||||
|
||||
"@react-three/fiber": ["@react-three/fiber@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA=="],
|
||||
@ -411,6 +532,22 @@
|
||||
|
||||
"@repo/ui": ["@repo/ui@workspace:packages/ui"],
|
||||
|
||||
"@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@supabase/auth-js": ["@supabase/auth-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-vD2YoS8E2iKIX0F7EwXTmqhUpaNsmbU6X2R0/NdFcs02oEfnHyNP/3M716f3wVJ2E5XHGiTFXki6lRckhJ0Thg=="],
|
||||
|
||||
"@supabase/functions-js": ["@supabase/functions-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-uTuOAKzs9R/IovW1krO0ZbUHSJnsnyJElTXIRhjJTqymIVGcHzkAYnBCJqd7468Fs/Foz1BQ7Dv6DCl05lr7ig=="],
|
||||
|
||||
"@supabase/postgrest-js": ["@supabase/postgrest-js@2.95.3", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-LTrRBqU1gOovxRm1vRXPItSMPBmEFqrfTqdPTRtzOILV4jPSueFz6pES5hpb4LRlkFwCPRmv3nQJ5N625V2Xrg=="],
|
||||
|
||||
"@supabase/realtime-js": ["@supabase/realtime-js@2.95.3", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-D7EAtfU3w6BEUxDACjowWNJo/ZRo7sDIuhuOGKHIm9FHieGeoJV5R6GKTLtga/5l/6fDr2u+WcW/m8I9SYmaIw=="],
|
||||
|
||||
"@supabase/storage-js": ["@supabase/storage-js@2.95.3", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-4GxkJiXI3HHWjxpC3sDx1BVrV87O0hfX+wvJdqGv67KeCu+g44SPnII8y0LL/Wr677jB7tpjAxKdtVWf+xhc9A=="],
|
||||
|
||||
"@supabase/supabase-js": ["@supabase/supabase-js@2.95.3", "", { "dependencies": { "@supabase/auth-js": "2.95.3", "@supabase/functions-js": "2.95.3", "@supabase/postgrest-js": "2.95.3", "@supabase/realtime-js": "2.95.3", "@supabase/storage-js": "2.95.3" } }, "sha512-Fukw1cUTQ6xdLiHDJhKKPu6svEPaCEDvThqCne3OaQyZvuq2qjhJAd91kJu3PXLG18aooCgYBaB6qQz35hhABg=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||
@ -449,12 +586,16 @@
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/google.maps": ["@types/google.maps@3.58.1", "", {}, "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="],
|
||||
|
||||
"@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
|
||||
|
||||
"@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
|
||||
@ -467,6 +608,8 @@
|
||||
|
||||
"@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg=="],
|
||||
@ -499,6 +642,8 @@
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
@ -531,14 +676,22 @@
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="],
|
||||
|
||||
"better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="],
|
||||
|
||||
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],
|
||||
|
||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||
|
||||
"bin-links": ["bin-links@6.0.0", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
@ -553,6 +706,8 @@
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="],
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
@ -561,6 +716,8 @@
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
@ -577,6 +734,8 @@
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
||||
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
||||
@ -595,6 +754,8 @@
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"detect-gpu": ["detect-gpu@5.0.70", "", { "dependencies": { "webgl-constants": "^1.1.1" } }, "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
@ -603,14 +764,32 @@
|
||||
|
||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"dotenv": ["dotenv@16.0.3", "", {}, "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="],
|
||||
|
||||
"draco3d": ["draco3d@1.5.7", "", {}, "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.39.3", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-EZ8ZpYvDIvKU9C56JYLOmUskazhad+uXZCTCRN4OnRMsL+xAJ05dv1eCpAG5xzhsm1hqiuC5kAZUCS924u2DTw=="],
|
||||
|
||||
"drizzle-zod": ["drizzle-zod@0.5.1", "", { "peerDependencies": { "drizzle-orm": ">=0.23.13", "zod": "*" } }, "sha512-C/8bvzUH/zSnVfwdSibOgFjLhtDtbKYmkbPbUCq46QZyZCH6kODIMSOgZ8R7rVjoI+tCj3k06MRJMDqsIeoS4A=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
@ -627,6 +806,10 @@
|
||||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||
|
||||
"esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="],
|
||||
|
||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
|
||||
@ -667,6 +850,8 @@
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
@ -681,6 +866,8 @@
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"framer-motion": ["framer-motion@12.26.2", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
@ -689,6 +876,8 @@
|
||||
|
||||
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||
|
||||
"gel": ["gel@2.2.0", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ=="],
|
||||
|
||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
@ -699,6 +888,8 @@
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
|
||||
|
||||
"glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
@ -729,6 +920,14 @@
|
||||
|
||||
"hls.js": ["hls.js@1.6.15", "", {}, "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA=="],
|
||||
|
||||
"html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", "htmlparser2": "^8.0.2", "selderee": "^0.11.0" } }, "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg=="],
|
||||
|
||||
"htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"iceberg-js": ["iceberg-js@0.8.1", "", {}, "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="],
|
||||
|
||||
"idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
@ -743,6 +942,8 @@
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||
@ -797,7 +998,7 @@
|
||||
|
||||
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
|
||||
|
||||
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
|
||||
|
||||
@ -805,6 +1006,8 @@
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
@ -819,8 +1022,14 @@
|
||||
|
||||
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
||||
|
||||
"kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="],
|
||||
|
||||
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
|
||||
@ -877,6 +1086,8 @@
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
"motion": ["motion@12.26.2", "", { "dependencies": { "framer-motion": "^12.26.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-2Q6g0zK1gUJKhGT742DAe42LgietcdiJ3L3OcYAHCQaC1UkLnn6aC8S/obe4CxYTLAgid2asS1QdQ/blYfo5dw=="],
|
||||
@ -889,10 +1100,18 @@
|
||||
|
||||
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||
|
||||
"nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"next": ["next@16.1.0", "", { "dependencies": { "@next/env": "16.1.0", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.0", "@next/swc-darwin-x64": "16.1.0", "@next/swc-linux-arm64-gnu": "16.1.0", "@next/swc-linux-arm64-musl": "16.1.0", "@next/swc-linux-x64-gnu": "16.1.0", "@next/swc-linux-x64-musl": "16.1.0", "@next/swc-win32-arm64-msvc": "16.1.0", "@next/swc-win32-x64-msvc": "16.1.0", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Y+KbmDbefYtHDDQKLNrmzE/YYzG2msqo2VXhzh5yrJ54tx/6TmGdkR5+kP9ma7i7LwZpZMfoY3m/AoPPPKxtVw=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
|
||||
|
||||
"npm-normalize-package-bin": ["npm-normalize-package-bin@5.0.0", "", {}, "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="],
|
||||
|
||||
"nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
@ -919,6 +1138,8 @@
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
@ -929,6 +1150,24 @@
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="],
|
||||
|
||||
"pg": ["pg@8.18.0", "", { "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.11.0", "", {}, "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
|
||||
|
||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
@ -937,10 +1176,24 @@
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="],
|
||||
|
||||
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||
|
||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"potpack": ["potpack@1.0.2", "", {}, "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
|
||||
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
|
||||
|
||||
"promise-worker-transferable": ["promise-worker-transferable@1.0.4", "", { "dependencies": { "is-promise": "^2.1.0", "lie": "^3.0.2" } }, "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
@ -955,6 +1208,8 @@
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"react-promise-suspense": ["react-promise-suspense@0.3.4", "", { "dependencies": { "fast-deep-equal": "^2.0.1" } }, "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
@ -963,18 +1218,26 @@
|
||||
|
||||
"react-use-measure": ["react-use-measure@2.1.7", "", { "peerDependencies": { "react": ">=16.13", "react-dom": ">=16.13" }, "optionalPeers": ["react-dom"] }, "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg=="],
|
||||
|
||||
"read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resend": ["resend@4.8.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA=="],
|
||||
|
||||
"resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
@ -985,8 +1248,12 @@
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||
@ -999,6 +1266,8 @@
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
@ -1007,10 +1276,18 @@
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="],
|
||||
@ -1031,6 +1308,10 @@
|
||||
|
||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||
|
||||
"supabase": ["supabase@2.75.3", "", { "dependencies": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", "tar": "7.5.7" }, "bin": { "supabase": "bin/supabase" } }, "sha512-j2uHJfK8TE7SxL6cd+kQixwkl8H5ASBkQfOEVjf3d3XS1Wixy4UO8WudDYNVriu1KuKzsNnkkSC/jTyHNXNrcQ=="],
|
||||
|
||||
"supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
@ -1043,6 +1324,8 @@
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
|
||||
"tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="],
|
||||
|
||||
"three": ["three@0.182.0", "", {}, "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="],
|
||||
|
||||
"three-bvh-csg": ["three-bvh-csg@0.0.17", "", { "peerDependencies": { "three": ">=0.151.0", "three-mesh-bvh": ">=0.6.6" } }, "sha512-iEkHDF8GRfGM6593Cuw8SnF1vfENCp46gIAtRzuL4nGXGWPcR1sbTBwM9ptDONb5twqlZp5WkAKya5aBKe2qcA=="],
|
||||
@ -1117,11 +1400,13 @@
|
||||
|
||||
"web": ["web@workspace:apps/editor"],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||
|
||||
"webgl-constants": ["webgl-constants@1.1.1", "", {}, "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="],
|
||||
|
||||
"webgl-sdf-generator": ["webgl-sdf-generator@1.1.1", "", {}, "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
"which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
|
||||
@ -1133,6 +1418,14 @@
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"write-file-atomic": ["write-file-atomic@7.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg=="],
|
||||
|
||||
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
||||
@ -1143,6 +1436,8 @@
|
||||
|
||||
"zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
@ -1187,14 +1482,20 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"gel/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
|
||||
|
||||
"sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"stats-gl/three": ["three@0.170.0", "", {}, "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ=="],
|
||||
@ -1207,8 +1508,54 @@
|
||||
|
||||
"web/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,11 @@
|
||||
"format:check": "biome format",
|
||||
"check": "biome check",
|
||||
"check:fix": "biome check --write",
|
||||
"check-types": "turbo run check-types"
|
||||
"check-types": "turbo run check-types",
|
||||
"db:start": "cd packages/db && supabase start",
|
||||
"db:stop": "cd packages/db && supabase stop",
|
||||
"db:reset": "cd packages/db && supabase db reset",
|
||||
"db:status": "cd packages/db && supabase status"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^10.7.7",
|
||||
@ -22,6 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.13",
|
||||
"supabase": "2.75.3",
|
||||
"turbo": "^2.8.1",
|
||||
"typescript": "5.9.2",
|
||||
"ultracite": "^7.1.1"
|
||||
|
||||
142
packages/auth/README.md
Normal file
142
packages/auth/README.md
Normal file
@ -0,0 +1,142 @@
|
||||
# @pascal-app/auth
|
||||
|
||||
Authentication package for Pascal Editor using Better Auth.
|
||||
|
||||
## Features
|
||||
|
||||
- **Magic Link Authentication** - Passwordless email-based authentication
|
||||
- **Session Management** - Secure cookie-based sessions
|
||||
- **Supabase Integration** - Uses Supabase as the database adapter
|
||||
- **Type-safe** - Full TypeScript support with type inference
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Configure environment variables
|
||||
|
||||
Add these to `apps/editor/.env.local`:
|
||||
|
||||
```bash
|
||||
# Better Auth
|
||||
BETTER_AUTH_SECRET=<generate_with_openssl_rand_base64_32>
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
Generate a secret for `BETTER_AUTH_SECRET`:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
### 2. Ensure database is running
|
||||
|
||||
Make sure you have Supabase running with the auth tables created. See `@pascal-app/db` package for setup.
|
||||
|
||||
## Usage
|
||||
|
||||
### Server-side (API routes, server actions)
|
||||
|
||||
```typescript
|
||||
import { auth } from '@pascal-app/auth/server'
|
||||
|
||||
// Get session in server component or action
|
||||
const session = await auth.api.getSession({ headers: request.headers })
|
||||
|
||||
if (!session) {
|
||||
return { error: 'Unauthorized' }
|
||||
}
|
||||
|
||||
// Access user data
|
||||
const userId = session.user.id
|
||||
const email = session.user.email
|
||||
```
|
||||
|
||||
### Client-side (React components)
|
||||
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { authClient } from '@pascal-app/auth/client'
|
||||
|
||||
function SignInButton() {
|
||||
const { signIn } = authClient
|
||||
|
||||
const handleSignIn = async (email: string) => {
|
||||
await signIn.magicLink({
|
||||
email,
|
||||
callbackURL: '/dashboard',
|
||||
})
|
||||
}
|
||||
|
||||
return <button onClick={() => handleSignIn('user@example.com')}>Sign In</button>
|
||||
}
|
||||
```
|
||||
|
||||
### Using the auth hook
|
||||
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { authClient } from '@pascal-app/auth/client'
|
||||
|
||||
function Profile() {
|
||||
const { data: session, isPending } = authClient.useSession()
|
||||
|
||||
if (isPending) return <div>Loading...</div>
|
||||
if (!session) return <div>Not signed in</div>
|
||||
|
||||
return <div>Signed in as {session.user.email}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## API Routes
|
||||
|
||||
The auth package requires an API route handler in your Next.js app:
|
||||
|
||||
```typescript
|
||||
// app/api/auth/[...all]/route.ts
|
||||
import { auth } from '@pascal-app/auth/server'
|
||||
import { toNextJsHandler } from 'better-auth/next-js'
|
||||
|
||||
export const { GET, POST } = toNextJsHandler(auth)
|
||||
```
|
||||
|
||||
This handles all Better Auth endpoints:
|
||||
- `/api/auth/sign-in/magic-link` - Send magic link
|
||||
- `/api/auth/sign-in/magic-link/verify` - Verify magic link
|
||||
- `/api/auth/sign-out` - Sign out
|
||||
- `/api/auth/session` - Get session
|
||||
- And more...
|
||||
|
||||
## Email Configuration
|
||||
|
||||
By default, magic links are logged to the console. To send actual emails, you'll need to configure an email provider in `packages/auth/src/server.ts`:
|
||||
|
||||
```typescript
|
||||
magicLink({
|
||||
sendMagicLink: async ({ email, url }) => {
|
||||
// Use Resend, SendGrid, or your preferred email service
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: 'Sign in to Pascal Editor',
|
||||
html: `Click here to sign in: <a href="${url}">${url}</a>`,
|
||||
})
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
The auth package requires these tables (created by `@pascal-app/db` migrations):
|
||||
|
||||
- `users` - User accounts
|
||||
- `sessions` - Active sessions
|
||||
- `accounts` - OAuth provider accounts (for future use)
|
||||
- `verification_tokens` - Magic link tokens
|
||||
|
||||
## Security
|
||||
|
||||
- Session cookies are httpOnly and secure (in production)
|
||||
- Sessions expire after 7 days
|
||||
- Session cache is enabled for 5 minutes to reduce database queries
|
||||
- Magic link tokens expire after 15 minutes
|
||||
- All sensitive operations require valid session tokens
|
||||
24
packages/auth/package.json
Normal file
24
packages/auth/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@pascal-app/auth",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./server": {
|
||||
"types": "./src/server.ts",
|
||||
"default": "./src/server.ts"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./src/client.ts",
|
||||
"default": "./src/client.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@pascal-app/db": "*",
|
||||
"better-auth": "^1.4.18",
|
||||
"resend": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/typescript-config": "*",
|
||||
"typescript": "5.9.2"
|
||||
}
|
||||
}
|
||||
37
packages/auth/src/client.ts
Normal file
37
packages/auth/src/client.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { magicLinkClient } from 'better-auth/client/plugins'
|
||||
import { createAuthClient } from 'better-auth/react'
|
||||
|
||||
/**
|
||||
* Get the auth base URL
|
||||
* In development: use the editor URL (localhost:3000)
|
||||
* In production: use the same origin
|
||||
*/
|
||||
function getAuthURL(): string {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.location.origin
|
||||
}
|
||||
|
||||
// SSR fallback
|
||||
return process.env.BETTER_AUTH_URL || 'http://localhost:3000'
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth client instance
|
||||
* Configured for magic link authentication
|
||||
*/
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: getAuthURL(),
|
||||
plugins: [magicLinkClient()],
|
||||
})
|
||||
|
||||
/**
|
||||
* Export types for use in components
|
||||
*/
|
||||
export type AuthState = {
|
||||
user: (typeof authClient)['$Infer']['Session']['user'] | null
|
||||
session: (typeof authClient)['$Infer']['Session']['session'] | null
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export type User = NonNullable<AuthState['user']>
|
||||
export type Session = NonNullable<AuthState['session']>
|
||||
76
packages/auth/src/server.ts
Normal file
76
packages/auth/src/server.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import type { Database } from '@pascal-app/db'
|
||||
import { schema } from '@pascal-app/db'
|
||||
import type { BetterAuthOptions } from 'better-auth'
|
||||
import { betterAuth } from 'better-auth'
|
||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
||||
import { magicLink } from 'better-auth/plugins'
|
||||
|
||||
export interface SendMagicLinkParams {
|
||||
email: string
|
||||
url: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export interface AuthConfig {
|
||||
db: Database
|
||||
appName: string
|
||||
baseURL: string
|
||||
secret: string
|
||||
/** Callback to send magic link emails */
|
||||
sendMagicLink?: (params: SendMagicLinkParams) => Promise<void>
|
||||
/** Additional plugins to add (e.g., nextCookies for web) */
|
||||
additionalPlugins?: BetterAuthOptions['plugins']
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Better Auth instance with full configuration including:
|
||||
* - Magic link authentication
|
||||
* - Custom session with activePropertyId
|
||||
* - Session cookie caching
|
||||
*/
|
||||
export function createAuth(config: AuthConfig): ReturnType<typeof betterAuth> {
|
||||
return betterAuth({
|
||||
appName: config.appName,
|
||||
baseURL: config.baseURL,
|
||||
secret: config.secret,
|
||||
basePath: '/api/auth',
|
||||
database: drizzleAdapter(config.db, {
|
||||
provider: 'pg',
|
||||
usePlural: true,
|
||||
schema,
|
||||
}),
|
||||
advanced: {
|
||||
database: {
|
||||
generateId: false, // Use our prefixed nanoid IDs from schema
|
||||
},
|
||||
},
|
||||
session: {
|
||||
// Session caching to reduce database queries
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 5 * 60, // Cache duration in seconds (5 minutes)
|
||||
},
|
||||
additionalFields: {
|
||||
// Additional fields for the session table
|
||||
activePropertyId: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
...(config.additionalPlugins ?? []),
|
||||
// Magic link authentication
|
||||
...(config.sendMagicLink
|
||||
? [
|
||||
magicLink({
|
||||
sendMagicLink: config.sendMagicLink,
|
||||
expiresIn: 300, // 5 minutes
|
||||
disableSignUp: false, // Allow new users to sign up via magic link
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export type Auth = ReturnType<typeof createAuth>
|
||||
9
packages/auth/tsconfig.json
Normal file
9
packages/auth/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
127
packages/db/README.md
Normal file
127
packages/db/README.md
Normal file
@ -0,0 +1,127 @@
|
||||
# @pascal-app/db
|
||||
|
||||
Database package for Pascal Editor with Supabase.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
From the monorepo root:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
This installs Supabase CLI as a dev dependency.
|
||||
|
||||
### 2. Start Supabase locally
|
||||
|
||||
From the monorepo root:
|
||||
|
||||
```bash
|
||||
bun db:start
|
||||
```
|
||||
|
||||
This will start a local Supabase instance with PostgreSQL, PostgREST, and Studio.
|
||||
|
||||
### 3. Check Supabase status
|
||||
|
||||
```bash
|
||||
bun db:status
|
||||
```
|
||||
|
||||
You'll see output like:
|
||||
|
||||
```
|
||||
API URL: http://127.0.0.1:54321
|
||||
DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
||||
Studio URL: http://127.0.0.1:54323
|
||||
Anon key: eyJh...
|
||||
Service role key: eyJh...
|
||||
```
|
||||
|
||||
### 4. Configure environment variables
|
||||
|
||||
Add these to `apps/editor/.env.local`:
|
||||
|
||||
```bash
|
||||
# Supabase
|
||||
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your_anon_key>
|
||||
SUPABASE_SERVICE_ROLE_KEY=<your_service_role_key>
|
||||
|
||||
# Better Auth
|
||||
BETTER_AUTH_SECRET=<generate_with_openssl_rand_base64_32>
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
Generate a secret for `BETTER_AUTH_SECRET`:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
Migrations are located in `supabase/migrations/`.
|
||||
|
||||
### Apply migrations
|
||||
|
||||
```bash
|
||||
bun db:reset # Resets and applies all migrations
|
||||
```
|
||||
|
||||
### Create a new migration
|
||||
|
||||
```bash
|
||||
cd packages/db
|
||||
bunx supabase migration new <migration_name>
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Auth Tables (Better Auth)
|
||||
|
||||
- `users` - User accounts
|
||||
- `sessions` - Active sessions
|
||||
- `accounts` - OAuth provider accounts
|
||||
- `verification_tokens` - Magic link tokens
|
||||
|
||||
### Application Tables
|
||||
|
||||
- `properties` - User properties
|
||||
- `properties_addresses` - Property addresses with Google Maps data
|
||||
- `properties_models` - Scene graph models (versions)
|
||||
|
||||
## Usage
|
||||
|
||||
### Client-side (with RLS)
|
||||
|
||||
```typescript
|
||||
import { supabase } from '@pascal-app/db/client'
|
||||
|
||||
// RLS policies automatically filter by authenticated user
|
||||
const { data } = await supabase.from('properties').select('*')
|
||||
```
|
||||
|
||||
### Server-side (service role)
|
||||
|
||||
```typescript
|
||||
import { supabaseAdmin } from '@pascal-app/db/server'
|
||||
|
||||
// Bypasses RLS - you must manually filter by user_id
|
||||
const { data } = await supabaseAdmin
|
||||
.from('properties')
|
||||
.select('*')
|
||||
.eq('owner_id', userId)
|
||||
```
|
||||
|
||||
## Supabase Studio
|
||||
|
||||
Access the local Supabase Studio at: http://127.0.0.1:54323
|
||||
|
||||
Use this to:
|
||||
- Browse tables and data
|
||||
- Run SQL queries
|
||||
- View logs
|
||||
- Manage RLS policies
|
||||
27
packages/db/package.json
Normal file
27
packages/db/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@pascal-app/db",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./src/types.ts",
|
||||
"default": "./src/types.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.95.3",
|
||||
"drizzle-orm": "^0.39.0",
|
||||
"drizzle-zod": "^0.5.1",
|
||||
"nanoid": "^5.0.9",
|
||||
"postgres": "^3.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/typescript-config": "*",
|
||||
"drizzle-kit": "^0.30.0",
|
||||
"typescript": "5.9.2"
|
||||
}
|
||||
}
|
||||
11
packages/db/src/drizzle.ts
Normal file
11
packages/db/src/drizzle.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import postgres from 'postgres'
|
||||
import * as schema from './schema'
|
||||
|
||||
const connectionString = process.env.POSTGRES_URL!
|
||||
|
||||
const client = postgres(connectionString, { prepare: false })
|
||||
|
||||
export const db = drizzle({ client, schema })
|
||||
|
||||
export type Database = typeof db
|
||||
56
packages/db/src/helpers.ts
Normal file
56
packages/db/src/helpers.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { type AnyColumn, type SQL, sql } from 'drizzle-orm'
|
||||
import { text, timestamp } from 'drizzle-orm/pg-core'
|
||||
import { customAlphabet } from 'nanoid'
|
||||
|
||||
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
const nanoid = customAlphabet(alphabet, 16)
|
||||
|
||||
/**
|
||||
* Generate a unique ID with optional prefix
|
||||
* @example createId('user') => 'user_Abc123...'
|
||||
*/
|
||||
export const createId = (prefix?: string) => {
|
||||
const id = nanoid()
|
||||
return prefix ? `${prefix}_${id}` : id
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary key column with auto-generated prefixed ID
|
||||
* @example id('user') => text('id').notNull().primaryKey().$defaultFn(() => createId('user'))
|
||||
*/
|
||||
export const id = (prefix?: string) =>
|
||||
text('id')
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => createId(prefix))
|
||||
.$type<string>()
|
||||
|
||||
export const createdAt = timestamp('created_at', { withTimezone: true }).notNull().defaultNow()
|
||||
|
||||
export const updatedAt = timestamp('updated_at', { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
|
||||
export const deletedAt = timestamp('deleted_at', { withTimezone: true })
|
||||
|
||||
/**
|
||||
* Standard timestamp columns for created_at and updated_at
|
||||
*/
|
||||
export const timestamps = {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
}
|
||||
|
||||
// Alias for backwards compatibility with existing code
|
||||
export const timestampsColumns = timestamps
|
||||
|
||||
export const timestampsColumnsSoftDelete = {
|
||||
...timestampsColumns,
|
||||
deletedAt,
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL helper for case-insensitive comparison
|
||||
*/
|
||||
export const lower = (column: AnyColumn): SQL => sql`lower(${column})`
|
||||
23
packages/db/src/index.ts
Normal file
23
packages/db/src/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Database package
|
||||
* Exports Drizzle ORM and types
|
||||
* Note: Supabase clients are kept in the app's lib directory to avoid build-time initialization
|
||||
*/
|
||||
|
||||
export type { Database as SupabaseDatabase } from './types'
|
||||
|
||||
// Drizzle exports
|
||||
export { type Database, db } from './drizzle'
|
||||
export * from './schema'
|
||||
|
||||
import * as dbSchema from './schema'
|
||||
export const schema = dbSchema
|
||||
export {
|
||||
createId,
|
||||
deletedAt,
|
||||
id,
|
||||
lower,
|
||||
timestamps,
|
||||
timestampsColumns,
|
||||
timestampsColumnsSoftDelete,
|
||||
} from './helpers'
|
||||
31
packages/db/src/schema/auth/accounts.ts
Normal file
31
packages/db/src/schema/auth/accounts.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestamps } from '../../helpers'
|
||||
import { users } from './users'
|
||||
|
||||
export const accounts = pgTable('auth_accounts', (t) => ({
|
||||
id: id('account'),
|
||||
userId: t
|
||||
.text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
providerId: t.text('provider_id').notNull(),
|
||||
accountId: t.text('account_id').notNull(),
|
||||
password: t.text('password'),
|
||||
accessToken: t.text('access_token'),
|
||||
refreshToken: t.text('refresh_token'),
|
||||
idToken: t.text('id_token'),
|
||||
accessTokenExpiresAt: t.timestamp('access_token_expires_at', {
|
||||
withTimezone: true,
|
||||
}),
|
||||
refreshTokenExpiresAt: t.timestamp('refresh_token_expires_at', {
|
||||
withTimezone: true,
|
||||
}),
|
||||
scope: t.text('scope'),
|
||||
...timestamps,
|
||||
})).enableRLS()
|
||||
|
||||
export type Account = typeof accounts.$inferSelect
|
||||
export type NewAccount = typeof accounts.$inferInsert
|
||||
export const insertAccountSchema = createInsertSchema(accounts)
|
||||
export const selectAccountSchema = createSelectSchema(accounts)
|
||||
15
packages/db/src/schema/auth/jwks.ts
Normal file
15
packages/db/src/schema/auth/jwks.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { createdAt, id } from '../../helpers'
|
||||
|
||||
export const jwks = pgTable('auth_jwks', (t) => ({
|
||||
id: id('jwks'),
|
||||
publicKey: t.text('public_key').notNull(),
|
||||
privateKey: t.text('private_key').notNull(),
|
||||
createdAt,
|
||||
})).enableRLS()
|
||||
|
||||
export type Jwks = typeof jwks.$inferSelect
|
||||
export type NewJwks = typeof jwks.$inferInsert
|
||||
export const insertJwksSchema = createInsertSchema(jwks)
|
||||
export const selectJwksSchema = createSelectSchema(jwks)
|
||||
28
packages/db/src/schema/auth/sessions.ts
Normal file
28
packages/db/src/schema/auth/sessions.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestamps } from '../../helpers'
|
||||
import { users } from './users'
|
||||
|
||||
export const sessions = pgTable('auth_sessions', (t) => ({
|
||||
id: id('session'),
|
||||
userId: t
|
||||
.text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
expiresAt: t.timestamp('expires_at', { withTimezone: true }),
|
||||
token: t.text('token').notNull(),
|
||||
ipAddress: t.text('ip_address'),
|
||||
userAgent: t.text('user_agent'),
|
||||
// Custom: active property for the session context
|
||||
activePropertyId: t.text('active_property_id'),
|
||||
// Admin plugin support: tracks who is impersonating this session
|
||||
impersonatedBy: t
|
||||
.text('impersonated_by')
|
||||
.references(() => users.id, { onDelete: 'set null' }),
|
||||
...timestamps,
|
||||
})).enableRLS()
|
||||
|
||||
export type Session = typeof sessions.$inferSelect
|
||||
export type NewSession = typeof sessions.$inferInsert
|
||||
export const insertSessionSchema = createInsertSchema(sessions)
|
||||
export const selectSessionSchema = createSelectSchema(sessions)
|
||||
28
packages/db/src/schema/auth/users.ts
Normal file
28
packages/db/src/schema/auth/users.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { pgEnum, pgTable, uniqueIndex } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, lower, timestampsColumns } from '../../helpers'
|
||||
|
||||
export const USER_ROLES = ['user', 'admin'] as const
|
||||
export const userRoles = pgEnum('auth_user_roles', USER_ROLES)
|
||||
|
||||
export const users = pgTable(
|
||||
'auth_users',
|
||||
(t) => ({
|
||||
id: id('user'),
|
||||
email: t.text('email').notNull(),
|
||||
emailVerified: t.boolean('email_verified').notNull().default(false),
|
||||
name: t.text('name').notNull(),
|
||||
image: t.text('image'),
|
||||
role: userRoles('role').notNull().default('user'),
|
||||
banned: t.boolean('banned').notNull().default(false),
|
||||
banReason: t.text('ban_reason'),
|
||||
banExpires: t.timestamp('ban_expires', { withTimezone: true }),
|
||||
...timestampsColumns,
|
||||
}),
|
||||
(t) => [uniqueIndex('email_unique_index').on(lower(t.email))],
|
||||
).enableRLS()
|
||||
|
||||
export type User = typeof users.$inferSelect
|
||||
export type NewUser = typeof users.$inferInsert
|
||||
export const insertUserSchema = createInsertSchema(users)
|
||||
export const selectUserSchema = createSelectSchema(users)
|
||||
22
packages/db/src/schema/auth/verifications.ts
Normal file
22
packages/db/src/schema/auth/verifications.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { index, pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestamps } from '../../helpers'
|
||||
|
||||
export const verifications = pgTable(
|
||||
'auth_verifications',
|
||||
(t) => ({
|
||||
id: id('verification'),
|
||||
value: t.text('value').notNull(),
|
||||
identifier: t.text('identifier').notNull(),
|
||||
expiresAt: t.timestamp('expires_at', {
|
||||
withTimezone: true,
|
||||
}),
|
||||
...timestamps,
|
||||
}),
|
||||
(t) => [index('verification_identifier_index').on(t.identifier)],
|
||||
).enableRLS()
|
||||
|
||||
export type Verification = typeof verifications.$inferSelect
|
||||
export type NewVerification = typeof verifications.$inferInsert
|
||||
export const insertVerificationSchema = createInsertSchema(verifications)
|
||||
export const selectVerificationSchema = createSelectSchema(verifications)
|
||||
11
packages/db/src/schema/index.ts
Normal file
11
packages/db/src/schema/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// Auth tables
|
||||
export * from './auth/accounts'
|
||||
export * from './auth/jwks'
|
||||
export * from './auth/sessions'
|
||||
export * from './auth/users'
|
||||
export * from './auth/verifications'
|
||||
|
||||
// Property tables
|
||||
export * from './properties/addresses'
|
||||
export * from './properties/models'
|
||||
export * from './properties/properties'
|
||||
53
packages/db/src/schema/properties/addresses.ts
Normal file
53
packages/db/src/schema/properties/addresses.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { pgTable, unique } from 'drizzle-orm/pg-core'
|
||||
import { z } from 'zod'
|
||||
import { id, timestampsColumns } from '../../helpers'
|
||||
|
||||
export const addresses = pgTable(
|
||||
'properties_addresses',
|
||||
(t) => ({
|
||||
id: id('address'),
|
||||
streetNumber: t.text('street_number'),
|
||||
route: t.text('route'),
|
||||
routeShort: t.text('route_short'),
|
||||
neighborhood: t.text('neighborhood'),
|
||||
city: t.text('city'),
|
||||
county: t.text('county'),
|
||||
state: t.text('state'),
|
||||
stateLong: t.text('state_long'),
|
||||
postalCode: t.text('postal_code'),
|
||||
postalCodeSuffix: t.text('postal_code_suffix'),
|
||||
country: t.text('country'),
|
||||
countryLong: t.text('country_long'),
|
||||
latitude: t.numeric('latitude'),
|
||||
longitude: t.numeric('longitude'),
|
||||
rawJson: t.jsonb('raw_json'),
|
||||
...timestampsColumns,
|
||||
}),
|
||||
(t) => [
|
||||
// Unique constraint on core address components to prevent duplicates
|
||||
unique('address_components_unique').on(t.streetNumber, t.route, t.city, t.state, t.postalCode),
|
||||
],
|
||||
).enableRLS()
|
||||
|
||||
// Create address schema manually to avoid issues with generated columns
|
||||
export const addressSchema = z.object({
|
||||
streetNumber: z.string().optional(),
|
||||
route: z.string().optional(),
|
||||
routeShort: z.string().optional(),
|
||||
neighborhood: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
county: z.string().optional(),
|
||||
state: z.string().optional(),
|
||||
stateLong: z.string().optional(),
|
||||
postalCode: z.string().optional(),
|
||||
postalCodeSuffix: z.string().optional(),
|
||||
country: z.string().default('US'),
|
||||
countryLong: z.string().optional(),
|
||||
latitude: z.string().optional(),
|
||||
longitude: z.string().optional(),
|
||||
rawJson: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export type AddressSchema = z.infer<typeof addressSchema>
|
||||
export type Address = typeof addresses.$inferSelect
|
||||
export type NewAddress = typeof addresses.$inferInsert
|
||||
31
packages/db/src/schema/properties/models.ts
Normal file
31
packages/db/src/schema/properties/models.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { relations } from 'drizzle-orm'
|
||||
import { pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestampsColumnsSoftDelete } from '../../helpers'
|
||||
import { properties } from './properties'
|
||||
|
||||
export const models = pgTable('properties_models', (t) => ({
|
||||
id: id('model'),
|
||||
name: t.text('name'),
|
||||
version: t.integer('version').default(1),
|
||||
description: t.text('description'),
|
||||
draft: t.boolean('draft').default(true),
|
||||
propertyId: t
|
||||
.text('property_id')
|
||||
.references(() => properties.id, { onDelete: 'set null' }),
|
||||
sceneGraph: t.jsonb('scene_graph'),
|
||||
metadata: t.jsonb('metadata'),
|
||||
...timestampsColumnsSoftDelete,
|
||||
})).enableRLS()
|
||||
|
||||
export const modelsRelations = relations(models, ({ one }) => ({
|
||||
property: one(properties, {
|
||||
fields: [models.propertyId],
|
||||
references: [properties.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export type Model = typeof models.$inferSelect
|
||||
export type NewModel = typeof models.$inferInsert
|
||||
export const insertModelSchema = createInsertSchema(models)
|
||||
export const selectModelSchema = createSelectSchema(models)
|
||||
44
packages/db/src/schema/properties/properties.ts
Normal file
44
packages/db/src/schema/properties/properties.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { relations } from 'drizzle-orm'
|
||||
import { index, pgTable } from 'drizzle-orm/pg-core'
|
||||
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
|
||||
import { id, timestampsColumns } from '../../helpers'
|
||||
import { users } from '../auth/users'
|
||||
import { addresses } from './addresses'
|
||||
|
||||
export const properties = pgTable(
|
||||
'properties',
|
||||
(t) => ({
|
||||
id: id('property'),
|
||||
name: t.text('name'),
|
||||
addressId: t
|
||||
.text('address_id')
|
||||
.references(() => addresses.id, { onDelete: 'set null' })
|
||||
.unique(),
|
||||
ownerId: t
|
||||
.text('owner_id')
|
||||
.references(() => users.id, { onDelete: 'set null' }),
|
||||
detailsJson: t.jsonb('details_json'),
|
||||
metadata: t.jsonb('metadata'),
|
||||
...timestampsColumns,
|
||||
}),
|
||||
(t) => [
|
||||
index('property_address_idx').on(t.addressId),
|
||||
index('property_owner_idx').on(t.ownerId),
|
||||
],
|
||||
).enableRLS()
|
||||
|
||||
export const propertiesRelations = relations(properties, ({ one }) => ({
|
||||
address: one(addresses, {
|
||||
fields: [properties.addressId],
|
||||
references: [addresses.id],
|
||||
}),
|
||||
owner: one(users, {
|
||||
fields: [properties.ownerId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export type Property = typeof properties.$inferSelect
|
||||
export type NewProperty = typeof properties.$inferInsert
|
||||
export const insertPropertySchema = createInsertSchema(properties)
|
||||
export const selectPropertySchema = createSelectSchema(properties)
|
||||
124
packages/db/src/types.ts
Normal file
124
packages/db/src/types.ts
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Database types for Supabase
|
||||
* Generated from database schema
|
||||
*/
|
||||
|
||||
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]
|
||||
|
||||
export interface Database {
|
||||
public: {
|
||||
Tables: {
|
||||
properties: {
|
||||
Row: {
|
||||
id: string
|
||||
name: string
|
||||
owner_id: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
id?: string
|
||||
name: string
|
||||
owner_id: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
name?: string
|
||||
owner_id?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
}
|
||||
properties_addresses: {
|
||||
Row: {
|
||||
id: string
|
||||
property_id: string
|
||||
formatted_address: string
|
||||
street_number: string | null
|
||||
route: string | null
|
||||
locality: string | null
|
||||
administrative_area_level_1: string | null
|
||||
administrative_area_level_2: string | null
|
||||
country: string | null
|
||||
postal_code: string | null
|
||||
latitude: number | null
|
||||
longitude: number | null
|
||||
place_id: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
id?: string
|
||||
property_id: string
|
||||
formatted_address: string
|
||||
street_number?: string | null
|
||||
route?: string | null
|
||||
locality?: string | null
|
||||
administrative_area_level_1?: string | null
|
||||
administrative_area_level_2?: string | null
|
||||
country?: string | null
|
||||
postal_code?: string | null
|
||||
latitude?: number | null
|
||||
longitude?: number | null
|
||||
place_id?: string | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
property_id?: string
|
||||
formatted_address?: string
|
||||
street_number?: string | null
|
||||
route?: string | null
|
||||
locality?: string | null
|
||||
administrative_area_level_1?: string | null
|
||||
administrative_area_level_2?: string | null
|
||||
country?: string | null
|
||||
postal_code?: string | null
|
||||
latitude?: number | null
|
||||
longitude?: number | null
|
||||
place_id?: string | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
}
|
||||
properties_models: {
|
||||
Row: {
|
||||
id: string
|
||||
property_id: string
|
||||
name: string
|
||||
version: number
|
||||
draft: boolean
|
||||
scene_graph: Json | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
Insert: {
|
||||
id?: string
|
||||
property_id: string
|
||||
name: string
|
||||
version?: number
|
||||
draft?: boolean
|
||||
scene_graph?: Json | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
property_id?: string
|
||||
name?: string
|
||||
version?: number
|
||||
draft?: boolean
|
||||
scene_graph?: Json | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
Views: {}
|
||||
Functions: {}
|
||||
Enums: {}
|
||||
}
|
||||
}
|
||||
1
packages/db/supabase/.branches/_current_branch
Normal file
1
packages/db/supabase/.branches/_current_branch
Normal file
@ -0,0 +1 @@
|
||||
main
|
||||
1
packages/db/supabase/.temp/cli-latest
Normal file
1
packages/db/supabase/.temp/cli-latest
Normal file
@ -0,0 +1 @@
|
||||
v2.75.0
|
||||
38
packages/db/supabase/config.toml
Normal file
38
packages/db/supabase/config.toml
Normal file
@ -0,0 +1,38 @@
|
||||
# Supabase local development configuration
|
||||
# This file is used by `supabase start` for local development
|
||||
|
||||
project_id = "pascal-editor"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
port = 54321
|
||||
schemas = ["public"]
|
||||
extra_search_path = ["public"]
|
||||
max_rows = 1000
|
||||
|
||||
[db]
|
||||
port = 54322
|
||||
shadow_port = 54320
|
||||
major_version = 17
|
||||
|
||||
[studio]
|
||||
enabled = true
|
||||
port = 54323
|
||||
|
||||
[inbucket]
|
||||
enabled = true
|
||||
port = 54324
|
||||
smtp_port = 54325
|
||||
pop3_port = 54326
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
site_url = "http://localhost:3000"
|
||||
additional_redirect_urls = ["http://localhost:3000"]
|
||||
jwt_expiry = 3600
|
||||
enable_signup = true
|
||||
|
||||
[auth.email]
|
||||
enable_signup = true
|
||||
double_confirm_changes = false
|
||||
enable_confirmations = false
|
||||
@ -0,0 +1,80 @@
|
||||
-- Better Auth tables (copied from working monorepo)
|
||||
-- Using auth_ prefix and snake_case columns
|
||||
|
||||
CREATE TYPE "public"."auth_user_roles" AS ENUM('user', 'admin');
|
||||
|
||||
CREATE TABLE "auth_accounts" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"provider_id" text NOT NULL,
|
||||
"account_id" text NOT NULL,
|
||||
"password" text,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"id_token" text,
|
||||
"access_token_expires_at" timestamp with time zone,
|
||||
"refresh_token_expires_at" timestamp with time zone,
|
||||
"scope" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "auth_accounts" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TABLE "auth_jwks" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"public_key" text NOT NULL,
|
||||
"private_key" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "auth_jwks" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TABLE "auth_sessions" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"expires_at" timestamp with time zone,
|
||||
"token" text NOT NULL,
|
||||
"ip_address" text,
|
||||
"user_agent" text,
|
||||
"impersonated_by" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "auth_sessions" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TABLE "auth_users" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"email_verified" boolean DEFAULT false NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"image" text,
|
||||
"role" "auth_user_roles" DEFAULT 'user' NOT NULL,
|
||||
"banned" boolean DEFAULT false NOT NULL,
|
||||
"ban_reason" text,
|
||||
"ban_expires" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "auth_users" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE TABLE "auth_verifications" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"identifier" text NOT NULL,
|
||||
"expires_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE "auth_verifications" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE "auth_accounts" ADD CONSTRAINT "auth_accounts_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
|
||||
ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
|
||||
ALTER TABLE "auth_sessions" ADD CONSTRAINT "auth_sessions_impersonated_by_auth_users_id_fk" FOREIGN KEY ("impersonated_by") REFERENCES "public"."auth_users"("id") ON DELETE set null ON UPDATE no action;
|
||||
|
||||
CREATE UNIQUE INDEX "email_unique_index" ON "auth_users" USING btree (lower("email"));
|
||||
@ -0,0 +1,165 @@
|
||||
-- Properties tables
|
||||
-- Simple property management without organizations
|
||||
|
||||
-- Properties addresses table (must be created first due to foreign key)
|
||||
CREATE TABLE IF NOT EXISTS properties_addresses (
|
||||
id TEXT PRIMARY KEY,
|
||||
street_number TEXT,
|
||||
route TEXT,
|
||||
route_short TEXT,
|
||||
neighborhood TEXT,
|
||||
city TEXT,
|
||||
county TEXT,
|
||||
state TEXT,
|
||||
state_long TEXT,
|
||||
postal_code TEXT,
|
||||
postal_code_suffix TEXT,
|
||||
country TEXT,
|
||||
country_long TEXT,
|
||||
latitude NUMERIC,
|
||||
longitude NUMERIC,
|
||||
raw_json JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(street_number, route, city, state, postal_code)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_properties_addresses_city_state ON properties_addresses(city, state);
|
||||
|
||||
-- Properties table
|
||||
CREATE TABLE IF NOT EXISTS properties (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
address_id TEXT UNIQUE REFERENCES properties_addresses(id) ON DELETE SET NULL,
|
||||
owner_id TEXT REFERENCES auth_users(id) ON DELETE SET NULL,
|
||||
details_json JSONB,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_properties_owner_id ON properties(owner_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_properties_address_id ON properties(address_id);
|
||||
|
||||
-- Properties models table (scene graphs)
|
||||
CREATE TABLE IF NOT EXISTS properties_models (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
version INTEGER DEFAULT 1,
|
||||
description TEXT,
|
||||
draft BOOLEAN DEFAULT true,
|
||||
property_id TEXT REFERENCES properties(id) ON DELETE SET NULL,
|
||||
scene_graph JSONB,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_properties_models_property_id ON properties_models(property_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_properties_models_version ON properties_models(property_id, version DESC);
|
||||
|
||||
-- Enable Row Level Security (RLS)
|
||||
ALTER TABLE properties ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE properties_addresses ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE properties_models ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS Policies for properties table
|
||||
CREATE POLICY "Users can view their own properties"
|
||||
ON properties FOR SELECT
|
||||
USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
|
||||
|
||||
CREATE POLICY "Users can insert their own properties"
|
||||
ON properties FOR INSERT
|
||||
WITH CHECK (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
|
||||
|
||||
CREATE POLICY "Users can update their own properties"
|
||||
ON properties FOR UPDATE
|
||||
USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
|
||||
|
||||
CREATE POLICY "Users can delete their own properties"
|
||||
ON properties FOR DELETE
|
||||
USING (owner_id = current_setting('app.user_id', true)::TEXT OR owner_id IS NULL);
|
||||
|
||||
-- RLS Policies for properties_addresses table
|
||||
-- Addresses table doesn't have property_id, so we'll allow all authenticated users
|
||||
CREATE POLICY "Authenticated users can view all addresses"
|
||||
ON properties_addresses FOR SELECT
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "Authenticated users can insert addresses"
|
||||
ON properties_addresses FOR INSERT
|
||||
WITH CHECK (true);
|
||||
|
||||
CREATE POLICY "Authenticated users can update addresses"
|
||||
ON properties_addresses FOR UPDATE
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "Authenticated users can delete addresses"
|
||||
ON properties_addresses FOR DELETE
|
||||
USING (true);
|
||||
|
||||
-- RLS Policies for properties_models table
|
||||
CREATE POLICY "Users can view models of their own properties"
|
||||
ON properties_models FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM properties
|
||||
WHERE properties.id = properties_models.property_id
|
||||
AND properties.owner_id = current_setting('app.user_id', true)::TEXT
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can insert models for their own properties"
|
||||
ON properties_models FOR INSERT
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM properties
|
||||
WHERE properties.id = properties_models.property_id
|
||||
AND properties.owner_id = current_setting('app.user_id', true)::TEXT
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can update models of their own properties"
|
||||
ON properties_models FOR UPDATE
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM properties
|
||||
WHERE properties.id = properties_models.property_id
|
||||
AND properties.owner_id = current_setting('app.user_id', true)::TEXT
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can delete models of their own properties"
|
||||
ON properties_models FOR DELETE
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM properties
|
||||
WHERE properties.id = properties_models.property_id
|
||||
AND properties.owner_id = current_setting('app.user_id', true)::TEXT
|
||||
)
|
||||
);
|
||||
|
||||
-- Trigger to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_properties_updated_at
|
||||
BEFORE UPDATE ON properties
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_properties_addresses_updated_at
|
||||
BEFORE UPDATE ON properties_addresses
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_properties_models_updated_at
|
||||
BEFORE UPDATE ON properties_models
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
@ -0,0 +1,2 @@
|
||||
-- Add active_property_id to sessions table
|
||||
ALTER TABLE auth_sessions ADD COLUMN IF NOT EXISTS active_property_id TEXT;
|
||||
9
packages/db/tsconfig.json
Normal file
9
packages/db/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user