diff --git a/apps/editor/app/api/auth/[...all]/route.ts b/apps/editor/app/api/auth/[...all]/route.ts deleted file mode 100644 index a2cd0932..00000000 --- a/apps/editor/app/api/auth/[...all]/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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 } diff --git a/apps/editor/app/api/health/route.ts b/apps/editor/app/api/health/route.ts new file mode 100644 index 00000000..300230ba --- /dev/null +++ b/apps/editor/app/api/health/route.ts @@ -0,0 +1,3 @@ +export function GET() { + return Response.json({ status: 'ok', app: 'editor', timestamp: new Date().toISOString() }) +} diff --git a/apps/editor/app/api/presets/[id]/route.ts b/apps/editor/app/api/presets/[id]/route.ts deleted file mode 100644 index 450ed884..00000000 --- a/apps/editor/app/api/presets/[id]/route.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { headers } from 'next/headers' -import { auth } from '@/lib/auth' -import { supabaseAdmin } from '@/lib/supabase/server' - -// PUT /api/presets/[id] -// Accepts any subset of: name, data, is_community -export async function PUT( - req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ headers: await headers() }) - if (!session?.user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { id } = await params - const body = await req.json() - const { name, data, is_community } = body - - if (name === undefined && data === undefined && is_community === undefined) { - return NextResponse.json({ error: 'Nothing to update' }, { status: 400 }) - } - - const existingResult = await supabaseAdmin - .from('presets') - .select('user_id') - .eq('id', id) - .single() - const existing = existingResult.data as { user_id: string | null } | null - - if (!existing || existing.user_id !== session.user.id) { - return NextResponse.json({ error: 'Not found or forbidden' }, { status: 403 }) - } - - const updates: Record = {} - if (name !== undefined) updates.name = name - if (data !== undefined) updates.data = data - if (is_community !== undefined) updates.is_community = is_community - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: preset, error } = await (supabaseAdmin as any) - .from('presets') - .update(updates) - .eq('id', id) - .select() - .single() - - if (error) return NextResponse.json({ error: error.message }, { status: 500 }) - return NextResponse.json({ preset }) -} - -// DELETE /api/presets/[id] -export async function DELETE( - req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ headers: await headers() }) - if (!session?.user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { id } = await params - - const existingResult = await supabaseAdmin - .from('presets') - .select('user_id, thumbnail_url') - .eq('id', id) - .single() - const existing = existingResult.data as { user_id: string | null; thumbnail_url: string | null } | null - - if (!existing || existing.user_id !== session.user.id) { - return NextResponse.json({ error: 'Not found or forbidden' }, { status: 403 }) - } - - // Delete thumbnail from storage if present - if (existing.thumbnail_url) { - const url = existing.thumbnail_url as string - const match = url.match(/preset-thumbnails\/(.+)$/) - if (match?.[1]) { - await supabaseAdmin.storage.from('preset-thumbnails').remove([match[1]]) - } - } - - const { error } = await supabaseAdmin.from('presets').delete().eq('id', id) - - if (error) return NextResponse.json({ error: error.message }, { status: 500 }) - return NextResponse.json({ success: true }) -} diff --git a/apps/editor/app/api/presets/[id]/thumbnail/route.ts b/apps/editor/app/api/presets/[id]/thumbnail/route.ts deleted file mode 100644 index 4f3a69f0..00000000 --- a/apps/editor/app/api/presets/[id]/thumbnail/route.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { headers } from 'next/headers' -import { auth } from '@/lib/auth' -import { supabaseAdmin } from '@/lib/supabase/server' - -// POST /api/presets/[id]/thumbnail -// Accepts a raw PNG blob, uploads to preset-thumbnails bucket, updates thumbnail_url -export async function POST( - req: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const session = await auth.api.getSession({ headers: await headers() }) - if (!session?.user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { id } = await params - - const existingResult = await supabaseAdmin - .from('presets') - .select('user_id') - .eq('id', id) - .single() - const existing = existingResult.data as { user_id: string | null } | null - - if (!existing || existing.user_id !== session.user.id) { - return NextResponse.json({ error: 'Not found or forbidden' }, { status: 403 }) - } - - const blob = await req.blob() - - const filename = `${id}/thumbnail.png` - const { data: uploadData, error: uploadError } = await supabaseAdmin.storage - .from('preset-thumbnails') - .upload(filename, blob, { - contentType: 'image/png', - upsert: true, - }) - - if (uploadError) { - return NextResponse.json({ error: uploadError.message }, { status: 500 }) - } - - const { data: urlData } = supabaseAdmin.storage - .from('preset-thumbnails') - .getPublicUrl(uploadData.path) - - const thumbnailUrl = `${urlData.publicUrl}?t=${Date.now()}` - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { error: updateError } = await (supabaseAdmin as any) - .from('presets') - .update({ thumbnail_url: thumbnailUrl }) - .eq('id', id) - - if (updateError) { - return NextResponse.json({ error: updateError.message }, { status: 500 }) - } - - return NextResponse.json({ thumbnail_url: thumbnailUrl }) -} diff --git a/apps/editor/app/api/presets/route.ts b/apps/editor/app/api/presets/route.ts deleted file mode 100644 index f8bb6a54..00000000 --- a/apps/editor/app/api/presets/route.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { headers } from 'next/headers' -import { auth } from '@/lib/auth' -import { supabaseAdmin } from '@/lib/supabase/server' -import { createId } from '@pascal-app/db' - -// GET /api/presets?type=door|window&tab=community|mine -export async function GET(req: NextRequest) { - const { searchParams } = req.nextUrl - const type = searchParams.get('type') - const tab = searchParams.get('tab') ?? 'community' - - if (!type || (type !== 'door' && type !== 'window')) { - return NextResponse.json({ error: 'Invalid type' }, { status: 400 }) - } - - if (tab === 'mine') { - const session = await auth.api.getSession({ headers: await headers() }) - if (!session?.user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { data, error } = await supabaseAdmin - .from('presets') - .select('*') - .eq('type', type) - .eq('user_id', session.user.id) - .order('created_at', { ascending: false }) - - if (error) return NextResponse.json({ error: error.message }, { status: 500 }) - return NextResponse.json({ presets: data }) - } - - // community tab - const { data, error } = await supabaseAdmin - .from('presets') - .select('*') - .eq('type', type) - .eq('is_community', true) - .order('created_at', { ascending: false }) - - if (error) return NextResponse.json({ error: error.message }, { status: 500 }) - return NextResponse.json({ presets: data }) -} - -// POST /api/presets -export async function POST(req: NextRequest) { - const session = await auth.api.getSession({ headers: await headers() }) - if (!session?.user) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const body = await req.json() - const { type, name, data, thumbnailUrl } = body - - if (!type || !name || !data) { - return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }) - } - - if (type !== 'door' && type !== 'window') { - return NextResponse.json({ error: 'Invalid type' }, { status: 400 }) - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { data: preset, error } = await (supabaseAdmin as any) - .from('presets') - .insert({ - id: createId('preset'), - type, - name, - data, - thumbnail_url: thumbnailUrl ?? null, - user_id: session.user.id, - is_community: false, - }) - .select() - .single() - - if (error) return NextResponse.json({ error: error.message }, { status: 500 }) - return NextResponse.json({ preset }, { status: 201 }) -} diff --git a/apps/editor/app/editor/[projectId]/error.tsx b/apps/editor/app/editor/[projectId]/error.tsx deleted file mode 100644 index 7ca2858b..00000000 --- a/apps/editor/app/editor/[projectId]/error.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' - -import Link from 'next/link' -import { useEffect } from 'react' - -export default function EditorRouteError({ - error, - reset, -}: Readonly<{ - error: Error & { digest?: string } - reset: () => void -}>) { - useEffect(() => { - console.error('[editor-route] Unhandled editor error:', error) - }, [error]) - - return ( -
-
-

Editor error

-

- We couldn't load this editor route. You can retry or return home. -

-
- - - Back to home - -
-
-
- ) -} diff --git a/apps/editor/app/editor/[projectId]/layout.tsx b/apps/editor/app/editor/[projectId]/layout.tsx deleted file mode 100644 index 61badebf..00000000 --- a/apps/editor/app/editor/[projectId]/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { Metadata } from 'next' - -export const metadata: Metadata = { - title: 'Editor Workspace', - description: 'Edit your Pascal projects in a full 3D workspace.', - robots: { - index: false, - follow: false, - }, -} - -export default function EditorProjectLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( -
- {children} -
- ) -} diff --git a/apps/editor/app/editor/[projectId]/page.tsx b/apps/editor/app/editor/[projectId]/page.tsx deleted file mode 100644 index d3b40e7e..00000000 --- a/apps/editor/app/editor/[projectId]/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client' - -import { useParams, useRouter } from 'next/navigation' -import { useEffect, useState } from 'react' -import Editor from '@/components/editor' -import { SceneLoader } from '@/components/ui/scene-loader' -import { useAuth } from '@/features/community/lib/auth/hooks' -import { useProjectStore } from '@/features/community/lib/projects/store' - -export default function EditorPage() { - const params = useParams() - const projectId = params.projectId as string - const { isAuthenticated, isLoading } = useAuth() - const setActiveProject = useProjectStore((state) => state.setActiveProject) - const router = useRouter() - const [mounted, setMounted] = useState(false) - - useEffect(() => { - setMounted(true) - }, []) - - // Use layoutEffect to set active project BEFORE the editor renders and hooks run - useEffect(() => { - if (isLoading) return - if (!isAuthenticated) { - router.replace('/') - return - } - if (projectId) { - setActiveProject(projectId) - } - }, [projectId, isAuthenticated, isLoading, setActiveProject, router]) - - if (!mounted || isLoading) { - return - } - - if (!isAuthenticated) { - return null - } - - return ( -
-
- -
-
- ) -} diff --git a/apps/editor/app/globals.css b/apps/editor/app/globals.css index c4c8a77b..03aa2509 100644 --- a/apps/editor/app/globals.css +++ b/apps/editor/app/globals.css @@ -1,12 +1,19 @@ @import "tailwindcss"; @import "tw-animate-css"; +@source "../../../packages/editor/src"; @custom-variant dark (&:is(.dark *)); @theme { - --font-sans: var(--font-barlow), var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; - --font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - --font-barlow: var(--font-barlow), var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + --font-sans: + var(--font-barlow), var(--font-geist-sans), ui-sans-serif, system-ui, + sans-serif; + --font-mono: + var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, + Consolas, monospace; + --font-barlow: + var(--font-barlow), var(--font-geist-sans), ui-sans-serif, system-ui, + sans-serif; } @theme inline { @@ -97,7 +104,9 @@ --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.235 0 0); /* slightly lighter than background (0.205) but darker than previous (0.269) */ + --accent: oklch( + 0.235 0 0 + ); /* slightly lighter than background (0.205) but darker than previous (0.269) */ --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); @@ -166,90 +175,189 @@ .pascal-loader-1 { width: 45px; aspect-ratio: 1; - --c:no-repeat linear-gradient(currentColor 0 0); + --c: no-repeat linear-gradient(currentColor 0 0); background: var(--c), var(--c), var(--c); - animation: + animation: pascal-l1-1 1s infinite, pascal-l1-2 1s infinite; } @keyframes pascal-l1-1 { - 0%,100% {background-size:20% 100%} - 33%,66% {background-size:20% 20%} + 0%, + 100% { + background-size: 20% 100%; + } + 33%, + 66% { + background-size: 20% 20%; + } } @keyframes pascal-l1-2 { - 0%,33% {background-position: 0 0,50% 50%,100% 100%} - 66%,100% {background-position: 100% 0,50% 50%,0 100%} + 0%, + 33% { + background-position: + 0 0, + 50% 50%, + 100% 100%; + } + 66%, + 100% { + background-position: + 100% 0, + 50% 50%, + 0 100%; + } } .pascal-loader-2 { width: 45px; - aspect-ratio: .75; + aspect-ratio: 0.75; --c: no-repeat linear-gradient(currentColor 0 0); - background: - var(--c) 0% 50%, - var(--c) 50% 50%, + background: + var(--c) 0% 50%, + var(--c) 50% 50%, var(--c) 100% 50%; background-size: 20% 50%; animation: pascal-l2 1s infinite linear; } @keyframes pascal-l2 { - 20% {background-position: 0% 0% ,50% 50% ,100% 50% } - 40% {background-position: 0% 100%,50% 0% ,100% 50% } - 60% {background-position: 0% 50% ,50% 100%,100% 0% } - 80% {background-position: 0% 50% ,50% 50% ,100% 100%} + 20% { + background-position: + 0% 0%, + 50% 50%, + 100% 50%; + } + 40% { + background-position: + 0% 100%, + 50% 0%, + 100% 50%; + } + 60% { + background-position: + 0% 50%, + 50% 100%, + 100% 0%; + } + 80% { + background-position: + 0% 50%, + 50% 50%, + 100% 100%; + } } .pascal-loader-3 { width: 45px; - aspect-ratio: .75; - --c:no-repeat linear-gradient(currentColor 0 0); - background: - var(--c) 0% 100%, - var(--c) 50% 100%, + aspect-ratio: 0.75; + --c: no-repeat linear-gradient(currentColor 0 0); + background: + var(--c) 0% 100%, + var(--c) 50% 100%, var(--c) 100% 100%; background-size: 20% 65%; animation: pascal-l3 1s infinite linear; } @keyframes pascal-l3 { - 16.67% {background-position: 0% 0% ,50% 100%,100% 100%} - 33.33% {background-position: 0% 0% ,50% 0% ,100% 100%} - 50% {background-position: 0% 0% ,50% 0% ,100% 0% } - 66.67% {background-position: 0% 100%,50% 0% ,100% 0% } - 83.33% {background-position: 0% 100%,50% 100%,100% 0% } + 16.67% { + background-position: + 0% 0%, + 50% 100%, + 100% 100%; + } + 33.33% { + background-position: + 0% 0%, + 50% 0%, + 100% 100%; + } + 50% { + background-position: + 0% 0%, + 50% 0%, + 100% 0%; + } + 66.67% { + background-position: + 0% 100%, + 50% 0%, + 100% 0%; + } + 83.33% { + background-position: + 0% 100%, + 50% 100%, + 100% 0%; + } } .pascal-loader-4 { width: 45px; aspect-ratio: 1; - --c:no-repeat linear-gradient(currentColor 0 0); + --c: no-repeat linear-gradient(currentColor 0 0); background: var(--c), var(--c), var(--c); - animation: + animation: pascal-l4-1 1s infinite, pascal-l4-2 1s infinite; } @keyframes pascal-l4-1 { - 0%,100% {background-size:20% 100%} - 33%,66% {background-size:20% 40%} + 0%, + 100% { + background-size: 20% 100%; + } + 33%, + 66% { + background-size: 20% 40%; + } } @keyframes pascal-l4-2 { - 0%,33% {background-position: 0 0,50% 100%,100% 100%} - 66%,100% {background-position: 100% 0,0 100%,50% 100%} + 0%, + 33% { + background-position: + 0 0, + 50% 100%, + 100% 100%; + } + 66%, + 100% { + background-position: + 100% 0, + 0 100%, + 50% 100%; + } } .pascal-loader-5 { width: 45px; aspect-ratio: 1; - --c:no-repeat linear-gradient(currentColor 0 0); + --c: no-repeat linear-gradient(currentColor 0 0); background: var(--c), var(--c), var(--c); - animation: + animation: pascal-l5-1 1s infinite, pascal-l5-2 1s infinite; } @keyframes pascal-l5-1 { - 0%,100% {background-size:20% 100%} - 33%,66% {background-size:20% 40%} + 0%, + 100% { + background-size: 20% 100%; + } + 33%, + 66% { + background-size: 20% 40%; + } } @keyframes pascal-l5-2 { - 0%,33% {background-position: 0 0 ,50% 100%,100% 0} - 66%,100% {background-position: 0 100%,50% 0 ,100% 100%} + 0%, + 33% { + background-position: + 0 0, + 50% 100%, + 100% 0; + } + 66%, + 100% { + background-position: + 0 100%, + 50% 0, + 100% 100%; + } } diff --git a/apps/editor/app/layout.tsx b/apps/editor/app/layout.tsx index ef3b645c..33db7f78 100644 --- a/apps/editor/app/layout.tsx +++ b/apps/editor/app/layout.tsx @@ -1,12 +1,7 @@ import type { Metadata } from 'next' -import Script from 'next/script' -import localFont from 'next/font/local' import { Barlow } from 'next/font/google' -import { Analytics } from '@vercel/analytics/react' -import { SpeedInsights } from '@vercel/speed-insights/next' -import { VercelToolbar } from '@vercel/toolbar/next' -import { UsernameGate } from '@/features/community/components/username-gate' -import { siteConfig } from './seo' +import localFont from 'next/font/local' +import Script from 'next/script' import './globals.css' const geistSans = localFont({ @@ -26,48 +21,8 @@ const barlow = Barlow({ }) export const metadata: Metadata = { - metadataBase: new URL(siteConfig.url), - title: { - default: siteConfig.name, - template: '%s | Pascal Editor', - }, - description: siteConfig.description, - applicationName: siteConfig.name, - keywords: [...siteConfig.keywords], - authors: [{ name: 'Pascal', url: 'https://pascal.app' }], - creator: 'Pascal', - publisher: 'Pascal', - alternates: { - canonical: '/', - }, - icons: [{ rel: 'icon', url: '/favicon.ico' }], - openGraph: { - title: siteConfig.name, - description: siteConfig.description, - url: siteConfig.url, - siteName: siteConfig.name, - images: [{ url: siteConfig.ogImage, alt: 'Pascal Editor' }], - locale: 'en_US', - type: 'website', - }, - twitter: { - card: 'summary_large_image', - title: siteConfig.name, - description: siteConfig.description, - creator: siteConfig.twitterHandle, - images: [siteConfig.ogImage], - }, - robots: { - index: true, - follow: true, - googleBot: { - index: true, - follow: true, - 'max-image-preview': 'large', - 'max-snippet': -1, - 'max-video-preview': -1, - }, - }, + title: 'Pascal Editor', + description: 'Standalone building editor', } export default function RootLayout({ @@ -75,32 +30,25 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode }>) { - const shouldShowToolbar = process.env.NODE_ENV === 'development' - return ( - + {process.env.NODE_ENV === 'development' && ( <>