diff --git a/CLAUDE.md b/CLAUDE.md index 79203a83..66994993 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,8 +80,7 @@ deliberate — it is the owner's own machine. Do not add a jail without being as src/ ├── server.tsx # the single entrypoint: routes, WS upgrades, static files ├── apps/ -│ ├── officer-web/ # the SPA shell — Screens/Authentication + Screens/Dashboard -│ └── landing/ # marketing landing page +│ └── officer-web/ # the SPA shell — Screens/Authentication + Screens/Dashboard ├── servers/ │ ├── hono.ts # router composition; everything under /api │ ├── _middlewares/ # auth, body parsing, the permission gate, rate limiting diff --git a/package.json b/package.json index 0269b1f3..105c00ea 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "prebuild": "bun run ./scripts/prebuild.ts", "build:web": "bun run ./scripts/build/web.ts", "build:dashboard": "bun run ./scripts/build/dashboard.ts", - "build:landing": "bun run ./scripts/build/landing.ts", "db:gen": "cd src/databases/officer_db && bun run generate", "db:push": "bun run scripts/gen-plugin-schemas.ts && cd src/databases/officer_db && bun run push", "db:migrate": "cd src/databases/officer_db && bun run migrate", diff --git a/scripts/build/landing.ts b/scripts/build/landing.ts deleted file mode 100644 index dab79f37..00000000 --- a/scripts/build/landing.ts +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bun -import plugin from 'bun-plugin-tailwind'; -import { existsSync } from 'fs'; -import { rm } from 'fs/promises'; -import path from 'path'; - -const outdir = path.join(process.cwd(), 'dist_landing'); - -if (existsSync(outdir)) { - console.log(`🗑️ Cleaning previous build at ${outdir}`); - await rm(outdir, { recursive: true, force: true }); -} - -console.log('\n🚀 Starting landing page build...\n'); - -const start = performance.now(); - -const entrypoints = [...new Bun.Glob('**.html').scanSync('src/apps/landing')] - .map((a) => path.resolve('src/apps/landing', a)) - .filter((dir) => !dir.includes('node_modules')); -console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? 'file' : 'files'} to process\n`); - -const formatFileSize = (bytes: number): string => { - const units = ['B', 'KB', 'MB', 'GB']; - let size = bytes; - let unitIndex = 0; - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; - } - return `${size.toFixed(2)} ${units[unitIndex]}`; -}; - -const result = await Bun.build({ - entrypoints, - outdir, - plugins: [plugin], - minify: true, - target: 'browser', - sourcemap: 'linked', - define: { - 'process.env.NODE_ENV': JSON.stringify('production'), - }, -}); - -const end = performance.now(); - -const outputTable = result.outputs.map((output) => ({ - File: path.relative(process.cwd(), output.path), - Type: output.kind, - Size: formatFileSize(output.size), -})); - -console.table(outputTable); -console.log(`\n✅ Build completed in ${(end - start).toFixed(2)}ms\n`); diff --git a/src/apps/landing/App.tsx b/src/apps/landing/App.tsx deleted file mode 100644 index 3f332613..00000000 --- a/src/apps/landing/App.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { PixelGrid } from '../../workspaces/components/PixelGrid'; -import { DuckAvatar } from './DuckAvatar'; -import { WaitlistForm } from './WaitlistForm'; - -const landscapeBg = '/landscape1.webp'; -const logoSrc = '/officer-logo.svg'; - -export const App = () => { - return ( -
- {/* Background */} -
- - -
- - {/* Logo */} -
-
- Officer Logo -
-
- Officer Logo -
-
- - {/* Waitlist form */} -
- -
-
- ); -}; diff --git a/src/apps/landing/DuckAvatar.tsx b/src/apps/landing/DuckAvatar.tsx deleted file mode 100644 index 6e27d5dc..00000000 --- a/src/apps/landing/DuckAvatar.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Canvas, useThree } from '@react-three/fiber'; -import { OrbitControls, useGLTF, useAnimations } from '@react-three/drei'; -import { useEffect, useState, useRef } from 'react'; - -function DuckModel({ onLoaded }: { onLoaded: () => void }) { - const character = useGLTF('/duck3D/Character_output.glb'); - const animations = useGLTF('/duck3D/Meshy_Merged_Animations.glb'); - const meshRef = useRef(null); - const { actions } = useAnimations(animations.animations, meshRef); - - useEffect(() => { - onLoaded(); - }, [character, animations, onLoaded]); - - useEffect(() => { - if (!actions) return; - const action = actions['Walking']; - if (!action) return; - action.reset(); - action.setLoop(2201, Infinity); - action.clampWhenFinished = false; - action.fadeIn(0.4); - action.play(); - }, [actions]); - - return ; -} - -function Camera() { - const { camera } = useThree(); - const controlsRef = useRef(null); - - useEffect(() => { - if (controlsRef.current) { - controlsRef.current.target.set(0.3, 3.29, -0.4); - camera.position.set(2.89, 5.24, 7.38); - controlsRef.current.update(); - } - }, [camera]); - - return ; -} - -export function DuckAvatar() { - const [isLoaded, setIsLoaded] = useState(false); - - return ( -
-
- - - - - - - setIsLoaded(true)} /> - -
-
- ); -} diff --git a/src/apps/landing/WaitlistForm.tsx b/src/apps/landing/WaitlistForm.tsx deleted file mode 100644 index aceb048a..00000000 --- a/src/apps/landing/WaitlistForm.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import type { CSSProperties, FormEvent } from 'react'; -import { useEffect, useRef, useState } from 'react'; - -const cardStyle: CSSProperties = { - backgroundColor: 'var(--card-bg)', - backgroundImage: ` - linear-gradient(to right, var(--card-grid-color) 1px, transparent 1px), - linear-gradient(to bottom, var(--card-grid-color) 1px, transparent 1px) - `, - backgroundSize: '20px 20px', -}; - -export function WaitlistForm() { - const [email, setEmail] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); - const [done, setDone] = useState(false); - const [focused, setFocused] = useState(false); - - const inputRef = useRef(null); - const wrapperRef = useRef(null); - const isValid = email.trim().length > 0 && email.includes('@'); - - useEffect(() => { - const vv = window.visualViewport; - if (!vv) return; - - const reset = () => { - window.scrollTo(0, 0); - document.documentElement.scrollTop = 0; - document.body.scrollTop = 0; - }; - - vv.addEventListener('resize', reset); - vv.addEventListener('scroll', reset); - return () => { - vv.removeEventListener('resize', reset); - vv.removeEventListener('scroll', reset); - }; - }, []); - - const handleFocus = () => setFocused(true); - - const handleBlur = () => { - setFocused(false); - window.scrollTo(0, 0); - }; - - const handleSubmit = async (ev: FormEvent) => { - ev.preventDefault(); - inputRef.current?.blur(); - const trimmed = email.trim(); - if (!trimmed || isSubmitting) return; - - setIsSubmitting(true); - try { - await fetch('https://alpha.officer.dev/api/waitlist', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: trimmed }), - }); - setDone(true); - } catch { - setDone(true); - } finally { - setIsSubmitting(false); - } - }; - - return ( -
- {done ? ( -
-

You're on the list!

-
-

- We'll notify {email} when Officer is ready. -

-
-
- ) : ( - <> -

Join the Waitlist

-

- Be the first to know when Officer launches. -

-
- setEmail(ev.target.value)} - onFocus={handleFocus} - onBlur={handleBlur} - placeholder="you@example.com" - className="flex-1 h-10 rounded-md border border-input bg-background px-4 py-2.5 text-base text-duck-dark placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" - /> - -
- - )} -
- ); -} diff --git a/src/apps/landing/index.html b/src/apps/landing/index.html deleted file mode 100644 index e8a8172d..00000000 --- a/src/apps/landing/index.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - Officer Dev (Alpha) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - diff --git a/src/apps/landing/main.tsx b/src/apps/landing/main.tsx deleted file mode 100644 index ed3b67f5..00000000 --- a/src/apps/landing/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App'; -import './styles.css'; - -createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/src/apps/landing/styles.css b/src/apps/landing/styles.css deleted file mode 100644 index a20ed246..00000000 --- a/src/apps/landing/styles.css +++ /dev/null @@ -1,92 +0,0 @@ -@import "tailwindcss"; -@plugin 'tailwindcss-animate'; - -@custom-variant dark (&:is(.dark *)); - -@theme { - --color-duck-yellow: var(--duck-yellow); - --color-duck-orange: var(--duck-orange); - --color-duck-teal: var(--duck-teal); - --color-duck-forest: var(--duck-forest); - --color-duck-dark: var(--duck-dark); - --color-duck-beige: var(--duck-beige); - - --color-background: hsl(var(--background)); - --color-foreground: hsl(var(--foreground)); - - --color-primary: hsl(var(--primary)); - --color-primary-foreground: hsl(var(--primary-foreground)); - - --color-muted: hsl(var(--muted)); - --color-muted-foreground: hsl(var(--muted-foreground)); - - --color-input: hsl(var(--input)); - --color-ring: hsl(var(--ring)); - - --radius-lg: var(--radius); - --radius-md: calc(var(--radius) - 2px); - --radius-sm: calc(var(--radius) - 4px); -} - -@layer base { - :root { - --duck-yellow: #F4C430; - --duck-orange: #EA580C; - --duck-teal: #0891B2; - --duck-forest: #166534; - --duck-dark: #14532D; - --duck-beige: #E7D4B5; - - --pixel-grid-color: rgba(20, 83, 45, 0.1); - - --card-bg: rgba(255, 255, 255, 0.9); - --card-grid-color: rgba(20, 83, 45, 0.08); - - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - - --radius: 0.5rem; - } - - .dark { - --duck-teal: #22d3ee; - --duck-dark: #f1f5f9; - --duck-forest: #4ade80; - --duck-beige: #292524; - - --pixel-grid-color: rgba(200, 230, 210, 0.08); - - --card-bg: rgba(10, 20, 15, 0.9); - --card-grid-color: rgba(200, 230, 210, 0.06); - - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - } - - html, body { - @apply bg-background text-foreground; - margin: 0; - overflow: hidden; - position: fixed; - width: 100%; - height: 100%; - } -}