diff --git a/apps/admin/index.html b/apps/admin/index.html new file mode 100644 index 0000000..49d4ec8 --- /dev/null +++ b/apps/admin/index.html @@ -0,0 +1,12 @@ + + + + + + XTablo Admin + + +
+ + + diff --git a/apps/admin/package.json b/apps/admin/package.json new file mode 100644 index 0000000..c431f5f --- /dev/null +++ b/apps/admin/package.json @@ -0,0 +1,46 @@ +{ + "name": "@xtablo/admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite dev --port 5176", + "build": "tsc -b && vite build --mode production", + "deploy": "wrangler deploy", + "typecheck": "tsc -b", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "preview": "vite preview", + "test": "vitest run --mode test --passWithNoTests", + "test:watch": "vitest watch --mode test --passWithNoTests", + "clean": "rm -rf dist .vite tsconfig.tsbuildinfo node_modules/.vite" + }, + "devDependencies": { + "@biomejs/biome": "2.2.5", + "@cloudflare/vite-plugin": "^1.9.4", + "@tailwindcss/vite": "^4.0.14", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "happy-dom": "^20.0.0", + "tailwindcss": "^4.0.14", + "tw-animate-css": "^1.4.0", + "typescript": "^5.7.0", + "vite": "^6.2.2", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4", + "wrangler": "^4.24.3" + }, + "dependencies": { + "@tanstack/react-query": "^5.69.0", + "@xtablo/shared": "workspace:*", + "@xtablo/shared-types": "workspace:*", + "@xtablo/ui": "workspace:*", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-router-dom": "^7.9.4" + } +} diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx new file mode 100644 index 0000000..d723e06 --- /dev/null +++ b/apps/admin/src/App.tsx @@ -0,0 +1,9 @@ +import AppRoutes from "./routes"; + +export default function App() { + return ( +
+ +
+ ); +} diff --git a/apps/admin/src/main.css b/apps/admin/src/main.css new file mode 100644 index 0000000..70631d1 --- /dev/null +++ b/apps/admin/src/main.css @@ -0,0 +1,30 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(0.97 0.01 95); + --foreground: oklch(0.2 0.02 255); + --card: oklch(0.995 0.002 95); + --card-foreground: oklch(0.2 0.02 255); + --border: oklch(0.88 0.01 95); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-border: var(--border); +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground antialiased; + } +} diff --git a/apps/admin/src/main.tsx b/apps/admin/src/main.tsx new file mode 100644 index 0000000..6781fde --- /dev/null +++ b/apps/admin/src/main.tsx @@ -0,0 +1,15 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App"; + +import "@xtablo/ui/globals.css"; +import "./main.css"; + +createRoot(document.getElementById("admin-root")!).render( + + + + + +); diff --git a/apps/admin/src/routes.test.tsx b/apps/admin/src/routes.test.tsx new file mode 100644 index 0000000..418cc42 --- /dev/null +++ b/apps/admin/src/routes.test.tsx @@ -0,0 +1,13 @@ +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import AppRoutes from "./routes"; + +it("renders the privileged gate on the root route", () => { + render( + + + + ); + + expect(screen.getByText(/admin access token/i)).toBeInTheDocument(); +}); diff --git a/apps/admin/src/routes.tsx b/apps/admin/src/routes.tsx new file mode 100644 index 0000000..3e30c96 --- /dev/null +++ b/apps/admin/src/routes.tsx @@ -0,0 +1,19 @@ +import { Route, Routes } from "react-router-dom"; + +function PrivilegedGatePlaceholder() { + return ( +
+
+

Admin access token required

+
+
+ ); +} + +export default function AppRoutes() { + return ( + + } /> + + ); +} diff --git a/apps/admin/src/setupTests.ts b/apps/admin/src/setupTests.ts new file mode 100644 index 0000000..b403818 --- /dev/null +++ b/apps/admin/src/setupTests.ts @@ -0,0 +1,6 @@ +import "@testing-library/jest-dom"; +import { cleanup } from "@testing-library/react"; + +afterEach(() => { + cleanup(); +}); diff --git a/apps/admin/tsconfig.json b/apps/admin/tsconfig.json new file mode 100644 index 0000000..586e2a9 --- /dev/null +++ b/apps/admin/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": ["vite/client", "vitest/globals"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@xtablo/ui": ["../../packages/ui/src"], + "@xtablo/ui/*": ["../../packages/ui/src/*"], + "@xtablo/shared": ["../../packages/shared/src"], + "@xtablo/shared/*": ["../../packages/shared/src/*"], + "@xtablo/shared-types": ["../../packages/shared-types/src"], + "@xtablo/shared-types/*": ["../../packages/shared-types/src/*"] + } + }, + "include": ["src"], + "references": [] +} diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts new file mode 100644 index 0000000..b80495d --- /dev/null +++ b/apps/admin/vite.config.ts @@ -0,0 +1,29 @@ +/// + +import { cloudflare } from "@cloudflare/vite-plugin"; +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig, type PluginOption } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => { + const plugins: PluginOption[] = [ + react(), + tailwindcss(), + tsconfigPaths({ ignoreConfigErrors: true }), + ]; + + if (mode !== "test" && process.env.VITEST !== "true") { + plugins.push(cloudflare({ inspectorPort: 9233 })); + } + + return { + plugins, + server: { cors: false }, + test: { + globals: true, + environment: "happy-dom", + setupFiles: "./src/setupTests.ts", + }, + }; +}); diff --git a/apps/admin/worker/index.ts b/apps/admin/worker/index.ts new file mode 100644 index 0000000..bee9dbb --- /dev/null +++ b/apps/admin/worker/index.ts @@ -0,0 +1,9 @@ +export default { + fetch(request: Request) { + const url = new URL(request.url); + if (url.pathname.startsWith("/api/")) { + return Response.json({ name: "XTablo Admin Worker" }); + } + return new Response(null, { status: 404 }); + }, +}; diff --git a/apps/admin/wrangler.toml b/apps/admin/wrangler.toml new file mode 100644 index 0000000..8083093 --- /dev/null +++ b/apps/admin/wrangler.toml @@ -0,0 +1,16 @@ +name = "xtablo-admin" +main = "worker/index.ts" +compatibility_date = "2025-07-09" + +[assets] +directory = "./dist/" +not_found_handling = "single-page-application" + +[observability] +enabled = true + +[env.staging] +route = { pattern = "admin-staging.internal.xtablo.com", custom_domain = true } + +[env.production] +route = { pattern = "admin.internal.xtablo.com", custom_domain = true } diff --git a/package.json b/package.json index bf99826..6c383b5 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,13 @@ "build:prod": "turbo build:prod --filter=@xtablo/main", "dev": "turbo dev", "dev:main": "turbo dev --filter=@xtablo/main", + "dev:admin": "turbo dev --filter=@xtablo/admin", "dev:external": "turbo dev --filter=@xtablo/external", "dev:clients": "turbo dev --filter=@xtablo/clients", "dev:api": "turbo dev --filter=@xtablo/api", "deploy:main:staging": "turbo deploy:staging --filter=@xtablo/main", "deploy:main:prod": "turbo deploy:prod --filter=@xtablo/main", + "deploy:admin": "turbo deploy --filter=@xtablo/admin", "deploy:chat": "turbo deploy --filter=@xtablo/chat-worker", "deploy:external": "turbo deploy --filter=@xtablo/external", "lint": "turbo lint",