diff --git a/.woodpecker.yml b/.woodpecker.yml index b4534d8..81c4bec 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -10,6 +10,36 @@ clone: steps: # 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: test-comp-3626/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/test-comp-3626-web + web=registry.threesix.ai/test-comp-3626/web:${CI_COMMIT_SHA:0:8} + -n projects + || echo "No deployment found for web, skipping" + when: + branch: main + event: push + # Woodpecker CI step for api service # Add this step to your .woodpecker.yml diff --git a/CLAUDE.md b/CLAUDE.md index a97a21c..c22a2d6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,4 +49,5 @@ test-comp-3626/ | Component | Type | Path | |-----------|------|------| | **api** | API service | `services/api/` | +| **web** | React app | `apps/web/` | diff --git a/Procfile b/Procfile index 60fb4f9..856758f 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,4 @@ # Local development processes # Components will be added below as they're created api: cd services/api && make run +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..e114e74 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,27 @@ +# Build stage +FROM node:20-alpine AS build + +WORKDIR /app + +# Copy package files +COPY apps/web/package*.json ./ + +# Install dependencies +RUN npm install + +# Copy source +COPY apps/web/ ./ + +# Build +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built assets +COPY --from=build /app/dist /usr/share/nginx/html +COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +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..8ad0a73 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,13 @@ + + + + + + + web | test-comp-3626 + + +
+ + + diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf new file mode 100644 index 0000000..2ab46b7 --- /dev/null +++ b/apps/web/nginx.conf @@ -0,0 +1,26 @@ +server { + listen 80; + 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..0d44779 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,34 @@ +{ + "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": { + "@test-comp-3626/logger": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.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..0537934 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,46 @@ +function App() { + return ( +
+
+
+

+ web +

+

+ Welcome to your React app. This is part of the{' '} + test-comp-3626{' '} + monorepo. +

+

+ Edit this file at{' '} + + apps/web/src/App.tsx + +

+
+ + React Docs + + + Vite Docs + + + View Source + +
+
+
+
+ ); +} + +export default App; diff --git a/apps/web/src/index.css b/apps/web/src/index.css new file mode 100644 index 0000000..17df0e7 --- /dev/null +++ b/apps/web/src/index.css @@ -0,0 +1,17 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/apps/web/src/lib/logger.ts b/apps/web/src/lib/logger.ts new file mode 100644 index 0000000..32a85cc --- /dev/null +++ b/apps/web/src/lib/logger.ts @@ -0,0 +1,11 @@ +import { createLogger, installGlobalHandlers } from '@test-comp-3626/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, + }, +});