diff --git a/.woodpecker.yml b/.woodpecker.yml
index 6707f82..08f7275 100644
--- a/.woodpecker.yml
+++ b/.woodpecker.yml
@@ -35,6 +35,33 @@ steps:
event: push
# COMPONENT_STEPS_BELOW
+
+ # Woodpecker CI step for web React app
+ # Add this step to your .woodpecker.yml
+
+ build-web:
+ image: woodpeckerci/plugin-kaniko
+ settings:
+ registry: registry.threesix.ai
+ repo: route-test-1770185086/web
+ tags:
+ - latest
+ - ${CI_COMMIT_SHA:0:8}
+ context: .
+ dockerfile: apps/web/Dockerfile
+ cache: true
+ skip-tls-verify: true
+ when:
+ branch: main
+ event: push
+
+ deploy-web:
+ image: bitnami/kubectl:latest
+ commands:
+ - kubectl set image deployment/route-test-1770185086-web web=registry.threesix.ai/route-test-1770185086/web:${CI_COMMIT_SHA:0:8} -n projects || echo "Deployment not found, skipping"
+ when:
+ branch: main
+ event: push
# Do not remove the marker above - component steps are inserted here
verify:
diff --git a/CLAUDE.md b/CLAUDE.md
index 3b2ffd9..24fe5e5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -76,4 +76,7 @@ route-test-1770185086/
## Components
-
+| Component | Type | Path |
+|-----------|------|------|
+| **web** | React app | `apps/web/` |
+
diff --git a/Procfile b/Procfile
index 8e897c6..e3c11fc 100644
--- a/Procfile
+++ b/Procfile
@@ -1,2 +1,3 @@
# Local development processes
# Components will be added below as they're created
+web: cd apps/web && npm run dev
diff --git a/apps/web/.eslintrc.cjs b/apps/web/.eslintrc.cjs
new file mode 100644
index 0000000..4c99537
--- /dev/null
+++ b/apps/web/.eslintrc.cjs
@@ -0,0 +1,18 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react-hooks/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+};
diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile
new file mode 100644
index 0000000..ab173e7
--- /dev/null
+++ b/apps/web/Dockerfile
@@ -0,0 +1,34 @@
+# Build stage - using pnpm for workspace dependency resolution
+FROM node:20-alpine AS build
+
+# Install pnpm
+RUN npm install -g pnpm
+
+WORKDIR /workspace
+
+# Copy workspace configuration files
+COPY pnpm-workspace.yaml package.json pnpm-lock.yaml* ./
+
+# Copy shared packages (required for workspace:* dependencies)
+COPY packages/ ./packages/
+
+# Copy the app component
+COPY apps/web/ ./apps/web/
+
+# Install dependencies using pnpm (resolves workspace:* correctly)
+RUN pnpm install --frozen-lockfile || pnpm install
+
+# Build the app
+WORKDIR /workspace/apps/web
+RUN pnpm build
+
+# Production stage
+FROM nginx:alpine
+
+# Copy built assets
+COPY --from=build /workspace/apps/web/dist /usr/share/nginx/html
+COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 3001
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/apps/web/component.yaml b/apps/web/component.yaml
new file mode 100644
index 0000000..33a535a
--- /dev/null
+++ b/apps/web/component.yaml
@@ -0,0 +1,6 @@
+name: web
+type: app
+port: 3001
+path: apps/web
+stack: react
+dependencies: []
diff --git a/apps/web/index.html b/apps/web/index.html
new file mode 100644
index 0000000..083202f
--- /dev/null
+++ b/apps/web/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ web | route-test-1770185086
+
+
+
+
+
+
diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf
new file mode 100644
index 0000000..1e23deb
--- /dev/null
+++ b/apps/web/nginx.conf
@@ -0,0 +1,26 @@
+server {
+ listen 3001;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Gzip compression
+ gzip on;
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
+
+ # Cache static assets
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+
+ # SPA fallback
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+}
diff --git a/apps/web/package.json b/apps/web/package.json
new file mode 100644
index 0000000..39723bd
--- /dev/null
+++ b/apps/web/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "web",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3001",
+ "build": "tsc && vite build",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview --port 3001",
+ "format": "prettier --write src/"
+ },
+ "dependencies": {
+ "@route-test-1770185086/logger": "workspace:*",
+ "@route-test-1770185086/ui": "workspace:*",
+ "@route-test-1770185086/layout": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.23.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@typescript-eslint/eslint-plugin": "^7.13.1",
+ "@typescript-eslint/parser": "^7.13.1",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.19",
+ "eslint": "^8.57.0",
+ "eslint-plugin-react-hooks": "^4.6.2",
+ "eslint-plugin-react-refresh": "^0.4.7",
+ "postcss": "^8.4.38",
+ "prettier": "^3.3.2",
+ "tailwindcss": "^3.4.4",
+ "typescript": "^5.5.3",
+ "vite": "^5.4.1"
+ }
+}
diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js
new file mode 100644
index 0000000..2aa7205
--- /dev/null
+++ b/apps/web/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/apps/web/public/vite.svg b/apps/web/public/vite.svg
new file mode 100644
index 0000000..6a41099
--- /dev/null
+++ b/apps/web/public/vite.svg
@@ -0,0 +1 @@
+
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
new file mode 100644
index 0000000..9811e8e
--- /dev/null
+++ b/apps/web/src/App.tsx
@@ -0,0 +1,113 @@
+import { DashboardShell, Sidebar, Header, type NavItem } from '@route-test-1770185086/layout';
+import {
+ Button,
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ Badge,
+ Home,
+ Users,
+ Settings,
+ BarChart3,
+} from '@route-test-1770185086/ui';
+
+const navItems: NavItem[] = [
+ { label: 'Dashboard', href: '/', icon: Home, active: true },
+ { label: 'Analytics', href: '/analytics', icon: BarChart3 },
+ { label: 'Users', href: '/users', icon: Users, badge: '12' },
+ { label: 'Settings', href: '/settings', icon: Settings },
+];
+
+function App() {
+ return (
+ route-test-1770185086
+ }
+ items={navItems}
+ footer={
+
+ v0.0.1
+
+ }
+ />
+ }
+ header={
+
+ }
+ >
+
+ {/* Welcome card */}
+
+
+ Welcome to web
+
+ This is part of the{' '}
+
+ route-test-1770185086
+ {' '}
+ monorepo, using the shared UI library and layout components.
+
+
+
+
+ Get Started
+ Documentation
+
+
+
+
+ {/* Stats cards */}
+
+
+
+ Total Users
+ 1,234
+
+
+ +12% from last month
+
+
+
+
+
+ Active Sessions
+ 567
+
+
+ Live
+
+
+
+
+
+ API Requests
+ 89.2k
+
+
+ High traffic
+
+
+
+
+ {/* Edit hint */}
+
+ Edit this file at{' '}
+
+ apps/web/src/App.tsx
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
new file mode 100644
index 0000000..7f5aa2c
--- /dev/null
+++ b/apps/web/src/index.css
@@ -0,0 +1,6 @@
+/* Import design system tokens */
+@import '@route-test-1770185086/ui/styles';
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/apps/web/src/lib/logger.ts b/apps/web/src/lib/logger.ts
new file mode 100644
index 0000000..bc844c5
--- /dev/null
+++ b/apps/web/src/lib/logger.ts
@@ -0,0 +1,11 @@
+import { createLogger, installGlobalHandlers } from '@route-test-1770185086/logger';
+
+export const logger = createLogger({
+ level: import.meta.env.DEV ? 'debug' : 'info',
+ service: 'web',
+ // Set endpoint to send logs to your backend:
+ // endpoint: '/api/logs',
+});
+
+// Install global error handlers
+installGlobalHandlers(logger);
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
new file mode 100644
index 0000000..174dfe8
--- /dev/null
+++ b/apps/web/src/main.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App.tsx';
+import './index.css';
+import './lib/logger';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/apps/web/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js
new file mode 100644
index 0000000..d21f1cd
--- /dev/null
+++ b/apps/web/tailwind.config.js
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
new file mode 100644
index 0000000..a7fc6fb
--- /dev/null
+++ b/apps/web/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json
new file mode 100644
index 0000000..97ede7e
--- /dev/null
+++ b/apps/web/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
new file mode 100644
index 0000000..051ab31
--- /dev/null
+++ b/apps/web/vite.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3001,
+ },
+ preview: {
+ port: 3001,
+ },
+});