diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..1a6cb8f --- /dev/null +++ b/.drone.yml @@ -0,0 +1,208 @@ +--- +kind: pipeline +type: kubernetes +name: main-pipeline + +# Триггер: запускать при изменениях в backend, frontend или .drone.yml +trigger: + branch: + - main + - master + event: + - push + +steps: +# ============================================================ +# СБОРКА ОБРАЗОВ (параллельно) +# ============================================================ + +# --- Сборка Backend образа --- +- name: build-backend + image: plugins/kaniko + when: + changeset: + includes: + - backend/** + - .drone.yml + excludes: + - backend/README.md + - backend/**/*.md + settings: + registry: registry.vigdorov.ru + repo: registry.vigdorov.ru/library/team-planner-backend + dockerfile: backend/Dockerfile + context: backend + tags: + - ${DRONE_COMMIT_SHA:0:7} + - latest + cache: true + cache_repo: registry.vigdorov.ru/library/team-planner-backend-cache + username: + from_secret: HARBOR_USER + password: + from_secret: HARBOR_PASSWORD + no_push_metadata: true + +# --- Сборка Frontend образа (параллельно с backend) --- +- name: build-frontend + image: plugins/kaniko + when: + changeset: + includes: + - frontend/** + - .drone.yml + excludes: + - frontend/README.md + - frontend/**/*.md + settings: + registry: registry.vigdorov.ru + repo: registry.vigdorov.ru/library/team-planner-frontend + dockerfile: frontend/Dockerfile + context: frontend + tags: + - ${DRONE_COMMIT_SHA:0:7} + - latest + cache: true + cache_repo: registry.vigdorov.ru/library/team-planner-frontend-cache + username: + from_secret: HARBOR_USER + password: + from_secret: HARBOR_PASSWORD + no_push_metadata: true + +# ============================================================ +# ДЕПЛОЙ (только после завершения ОБЕИХ сборок) +# ============================================================ + +# --- Развертывание Backend в PROD --- +- name: deploy-backend + image: alpine/k8s:1.28.2 + depends_on: + - build-backend + - build-frontend + when: + changeset: + includes: + - backend/** + - .drone.yml + excludes: + - backend/README.md + - backend/**/*.md + environment: + KUBE_CONFIG_CONTENT: + from_secret: KUBE_CONFIG + commands: + - mkdir -p ~/.kube + - echo "$KUBE_CONFIG_CONTENT" > ~/.kube/config + - chmod 600 ~/.kube/config + - sed -i "s|https://127.0.0.1:6443|https://10.10.10.100:6443|g" ~/.kube/config + - export APP_NAMESPACE="team-planner" + - export IMAGE_TAG="${DRONE_COMMIT_SHA:0:7}" + - export BACKEND_IMAGE="registry.vigdorov.ru/library/team-planner-backend" + - kubectl cluster-info + - sed -e "s|__BACKEND_IMAGE__|$BACKEND_IMAGE:$IMAGE_TAG|g" k8s/backend-deployment.yaml | kubectl apply -n $APP_NAMESPACE -f - + - kubectl apply -n $APP_NAMESPACE -f k8s/backend-service.yaml + - kubectl rollout status deployment/team-planner-backend -n $APP_NAMESPACE --timeout=300s + - echo "✅ Backend deployed to PROD (image:$IMAGE_TAG)" + +# --- Развертывание Frontend в PROD --- +- name: deploy-frontend + image: alpine/k8s:1.28.2 + depends_on: + - build-backend + - build-frontend + when: + changeset: + includes: + - frontend/** + - .drone.yml + excludes: + - frontend/README.md + - frontend/**/*.md + environment: + KUBE_CONFIG_CONTENT: + from_secret: KUBE_CONFIG + commands: + - mkdir -p ~/.kube + - echo "$KUBE_CONFIG_CONTENT" > ~/.kube/config + - chmod 600 ~/.kube/config + - sed -i "s|https://127.0.0.1:6443|https://10.10.10.100:6443|g" ~/.kube/config + - export APP_NAMESPACE="team-planner" + - export IMAGE_TAG="${DRONE_COMMIT_SHA:0:7}" + - export FRONTEND_IMAGE="registry.vigdorov.ru/library/team-planner-frontend" + - kubectl cluster-info + - sed -e "s|__FRONTEND_IMAGE__|$FRONTEND_IMAGE:$IMAGE_TAG|g" k8s/frontend-deployment.yaml | kubectl apply -n $APP_NAMESPACE -f - + - kubectl apply -n $APP_NAMESPACE -f k8s/frontend-service.yaml + - kubectl rollout status deployment/team-planner-frontend -n $APP_NAMESPACE --timeout=300s + - echo "✅ Frontend deployed to PROD (image:$IMAGE_TAG)" + +--- +kind: pipeline +type: kubernetes +name: infra-pipeline + +# Триггер: запускать только при изменениях в k8s конфигах +trigger: + branch: + - main + - master + event: + - push + paths: + include: + - k8s/** + +steps: +# --- Создание секретов (УДАЛИТЬ ПОСЛЕ ПЕРВОГО ДЕПЛОЯ) --- +- name: create-secrets + image: alpine/k8s:1.28.2 + environment: + KUBE_CONFIG_CONTENT: + from_secret: KUBE_CONFIG + DB_NAME: + from_secret: DB_NAME + DB_USER: + from_secret: DB_USER + DB_PASSWORD: + from_secret: DB_PASSWORD + commands: + - mkdir -p ~/.kube + - echo "$KUBE_CONFIG_CONTENT" > ~/.kube/config + - chmod 600 ~/.kube/config + - sed -i "s|https://127.0.0.1:6443|https://10.10.10.100:6443|g" ~/.kube/config + - export APP_NAMESPACE="team-planner" + - kubectl create namespace $APP_NAMESPACE --dry-run=client -o yaml | kubectl apply -f - + - | + kubectl create secret generic team-planner-secrets \ + --from-literal=db-name="$DB_NAME" \ + --from-literal=db-user="$DB_USER" \ + --from-literal=db-password="$DB_PASSWORD" \ + --namespace=$APP_NAMESPACE \ + --dry-run=client -o yaml | kubectl apply -f - + - echo "✅ Secrets created/updated" + +# --- Развертывание инфраструктуры (PostgreSQL, Services, Ingress) --- +- name: deploy-infra + image: alpine/k8s:1.28.2 + depends_on: + - create-secrets + environment: + KUBE_CONFIG_CONTENT: + from_secret: KUBE_CONFIG + commands: + - mkdir -p ~/.kube + - echo "$KUBE_CONFIG_CONTENT" > ~/.kube/config + - chmod 600 ~/.kube/config + - sed -i "s|https://127.0.0.1:6443|https://10.10.10.100:6443|g" ~/.kube/config + - export APP_NAMESPACE="team-planner" + - export HOSTNAME="team-planner.vigdorov.ru" + - export SECRET_NAME="wildcard-cert" + - kubectl cluster-info + - kubectl create namespace $APP_NAMESPACE --dry-run=client -o yaml | kubectl apply -f - + - kubectl apply -n $APP_NAMESPACE -f k8s/postgres-pvc.yaml + - kubectl apply -n $APP_NAMESPACE -f k8s/postgres-statefulset.yaml + - kubectl apply -n $APP_NAMESPACE -f k8s/postgres-service.yaml + - kubectl apply -n $APP_NAMESPACE -f k8s/backend-service.yaml + - kubectl apply -n $APP_NAMESPACE -f k8s/frontend-service.yaml + - sed -e "s|__HOSTNAME__|$HOSTNAME|g" -e "s|__SECRET_NAME__|$SECRET_NAME|g" k8s/ingress.yaml | kubectl apply -n $APP_NAMESPACE -f - + - echo "✅ Infrastructure updated" diff --git a/.gitignore b/.gitignore index 65b450b..e0ab4f2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ node_modules **/*/dist/ **/dist-ssr/ +.serena/ +.claude/ + # Environment .env .env.local @@ -29,3 +32,6 @@ pnpm-debug.log* # OS .DS_Store Thumbs.db + +# Kubernetes secrets +k8s/secrets.yaml diff --git a/.serena/.gitignore b/.serena/.gitignore deleted file mode 100644 index 14d86ad..0000000 --- a/.serena/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/cache diff --git a/.serena/project.yml b/.serena/project.yml deleted file mode 100644 index a70f8e4..0000000 --- a/.serena/project.yml +++ /dev/null @@ -1,84 +0,0 @@ -# list of languages for which language servers are started; choose from: -# al bash clojure cpp csharp csharp_omnisharp -# dart elixir elm erlang fortran go -# haskell java julia kotlin lua markdown -# nix perl php python python_jedi r -# rego ruby ruby_solargraph rust scala swift -# terraform typescript typescript_vts yaml zig -# Note: -# - For C, use cpp -# - For JavaScript, use typescript -# Special requirements: -# - csharp: Requires the presence of a .sln file in the project folder. -# When using multiple languages, the first language server that supports a given file will be used for that file. -# The first language is the default language and the respective language server will be used as a fallback. -# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. -languages: -- typescript - -# the encoding used by text files in the project -# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings -encoding: "utf-8" - -# whether to use the project's gitignore file to ignore files -# Added on 2025-04-07 -ignore_all_files_in_gitignore: true - -# list of additional paths to ignore -# same syntax as gitignore, so you can use * and ** -# Was previously called `ignored_dirs`, please update your config if you are using that. -# Added (renamed) on 2025-04-07 -ignored_paths: [] - -# whether the project is in read-only mode -# If set to true, all editing tools will be disabled and attempts to use them will result in an error -# Added on 2025-04-18 -read_only: false - -# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. -# Below is the complete list of tools for convenience. -# To make sure you have the latest list of tools, and to view their descriptions, -# execute `uv run scripts/print_tool_overview.py`. -# -# * `activate_project`: Activates a project by name. -# * `check_onboarding_performed`: Checks whether project onboarding was already performed. -# * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_lines`: Deletes a range of lines within a file. -# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. -# * `execute_shell_command`: Executes a shell command. -# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. -# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). -# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). -# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. -# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. -# * `initial_instructions`: Gets the initial instructions for the current project. -# Should only be used in settings where the system prompt cannot be set, -# e.g. in clients you have no control over, like Claude Desktop. -# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_at_line`: Inserts content at a given line in a file. -# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. -# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: Lists memories in Serena's project-specific memory store. -# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). -# * `read_file`: Reads a file within the project directory. -# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. -# * `remove_project`: Removes a project from the Serena configuration. -# * `replace_lines`: Replaces a range of lines within a file with new content. -# * `replace_symbol_body`: Replaces the full definition of a symbol. -# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. -# * `search_for_pattern`: Performs a search for a pattern in the project. -# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. -# * `switch_modes`: Activates modes by providing a list of their names -# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. -# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. -# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. -excluded_tools: [] - -# initial prompt for the project. It will always be given to the LLM upon activating the project -# (contrary to the memories, which are loaded on demand). -initial_prompt: "" - -project_name: "team-planner" -included_optional_tools: [] diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..7a41a77 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,48 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install ALL dependencies (including devDependencies for build) +RUN npm install --include=dev + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Install only production dependencies +COPY package*.json ./ +RUN npm install --only=production && npm cache clean --force + +# Copy built application from builder +COPY --from=builder /app/dist ./dist + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nestjs -u 1001 + +# Change ownership +RUN chown -R nestjs:nodejs /app + +# Switch to non-root user +USER nestjs + +# Expose port +EXPOSE 4001 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \ + CMD node -e "require('http').get('http://localhost:4001/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" + +# Start the application +CMD ["node", "dist/main"] diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs index 4e9f827..a0934dd 100644 --- a/backend/eslint.config.mjs +++ b/backend/eslint.config.mjs @@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'; export default tseslint.config( { - ignores: ['eslint.config.mjs'], + ignores: ['eslint.config.mjs', 'coverage'], }, eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, @@ -27,8 +27,12 @@ export default tseslint.config( { rules: { '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-unsafe-argument': 'error', + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-return': 'error', "prettier/prettier": ["error", { endOfLine: "auto" }], }, }, diff --git a/backend/package.json b/backend/package.json index b891f96..41963b6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,12 +13,13 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "lint": "tsc --noEmit && eslint \"{src,apps,libs,test}/**/*.ts\" && prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "clean": "rm -rf dist node_modules" }, "dependencies": { "@nestjs/common": "^11.0.1", diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts index cce879e..7e334d7 100644 --- a/backend/src/app.controller.ts +++ b/backend/src/app.controller.ts @@ -9,4 +9,9 @@ export class AppController { getHello(): string { return this.appService.getHello(); } + + @Get('health') + health(): { status: string } { + return { status: 'ok' }; + } } diff --git a/backend/src/ideas/dto/create-idea.dto.ts b/backend/src/ideas/dto/create-idea.dto.ts index d184cf2..9f62520 100644 --- a/backend/src/ideas/dto/create-idea.dto.ts +++ b/backend/src/ideas/dto/create-idea.dto.ts @@ -1,9 +1,4 @@ -import { - IsString, - IsOptional, - IsEnum, - MaxLength, -} from 'class-validator'; +import { IsString, IsOptional, IsEnum, MaxLength } from 'class-validator'; import { IdeaStatus, IdeaPriority } from '../entities/idea.entity'; export class CreateIdeaDto { diff --git a/backend/src/ideas/entities/idea.entity.ts b/backend/src/ideas/entities/idea.entity.ts index cdb2e71..a726d5a 100644 --- a/backend/src/ideas/entities/idea.entity.ts +++ b/backend/src/ideas/entities/idea.entity.ts @@ -49,7 +49,12 @@ export class Idea { @Column({ type: 'varchar', length: 100, nullable: true }) module: string | null; - @Column({ name: 'target_audience', type: 'varchar', length: 255, nullable: true }) + @Column({ + name: 'target_audience', + type: 'varchar', + length: 255, + nullable: true, + }) targetAudience: string | null; @Column({ type: 'text', nullable: true }) diff --git a/backend/src/ideas/ideas.service.ts b/backend/src/ideas/ideas.service.ts index 62be32a..e0a92e2 100644 --- a/backend/src/ideas/ideas.service.ts +++ b/backend/src/ideas/ideas.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, Like, FindOptionsWhere } from 'typeorm'; +import { Repository, FindOptionsWhere } from 'typeorm'; import { Idea } from './entities/idea.entity'; import { CreateIdeaDto, UpdateIdeaDto, QueryIdeasDto } from './dto'; @@ -119,7 +119,7 @@ export class IdeasService { .createQueryBuilder('idea') .select('DISTINCT idea.module', 'module') .where('idea.module IS NOT NULL') - .getRawMany(); + .getRawMany<{ module: string }>(); return result.map((r) => r.module).filter(Boolean); } diff --git a/backend/src/main.ts b/backend/src/main.ts index 7edcd73..cf1bafd 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -29,4 +29,4 @@ async function bootstrap() { await app.listen(port); console.log(`Backend running on http://localhost:${port}`); } -bootstrap(); +void bootstrap(); diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..a20502b --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..13b8e0a --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,40 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm install + +# Copy source code +COPY . . + +# Build argument for API URL (optional, defaults to empty for production) +# Empty value means use relative paths, which works with nginx proxy +ARG VITE_API_URL="" +ENV VITE_API_URL=$VITE_API_URL + +# Build the application +RUN npm run build + +# Production stage +FROM nginx:alpine + +# Copy built files from builder +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ + CMD wget --quiet --tries=1 --spider http://localhost/health || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 5e6b472..0742baf 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -6,18 +6,36 @@ import tseslint from 'typescript-eslint' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(['dist', 'coverage', '*.config.ts', '*.config.js']), { files: ['**/*.{ts,tsx}'], extends: [ js.configs.recommended, - tseslint.configs.recommended, + ...tseslint.configs.strictTypeChecked, reactHooks.configs.flat.recommended, reactRefresh.configs.vite, ], languageOptions: { ecmaVersion: 2020, globals: globals.browser, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-unnecessary-condition': 'error', + '@typescript-eslint/prefer-nullish-coalescing': 'error', + '@typescript-eslint/no-confusing-void-expression': 'off', + '@typescript-eslint/no-misused-promises': [ + 'error', + { + checksVoidReturn: { + attributes: false, + }, + }, + ], + 'react-hooks/set-state-in-effect': 'warn', }, }, ]) diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..7fb2a15 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,62 @@ +events {} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Performance + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; + + server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Health check endpoint for k8s + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Proxy API requests to backend + location /api/ { + proxy_pass http://team-planner-backend-service:4001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # SPA fallback - all routes go to index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Static assets with caching + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } +} diff --git a/frontend/package.json b/frontend/package.json index 1484f2b..91c2b73 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,8 +6,10 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" + "lint": "tsc -b --noEmit && eslint . && prettier --check \"src/**/*.{ts,tsx,json,css,md}\"", + "format": "prettier --write \"src/**/*.{ts,tsx,json,css,md}\"", + "preview": "vite preview", + "clean": "rm -rf dist node_modules" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -34,6 +36,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "prettier": "^3.4.2", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 932ab8e..904c414 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,14 @@ function App() { return ( - + Team Planner diff --git a/frontend/src/components/CreateIdeaModal/CreateIdeaModal.tsx b/frontend/src/components/CreateIdeaModal/CreateIdeaModal.tsx index 08bf217..e398e06 100644 --- a/frontend/src/components/CreateIdeaModal/CreateIdeaModal.tsx +++ b/frontend/src/components/CreateIdeaModal/CreateIdeaModal.tsx @@ -69,7 +69,12 @@ export function CreateIdeaModal() { }; return ( - +
Create New Idea @@ -163,7 +168,9 @@ export function CreateIdeaModal() { handleChange('verificationMethod', e.target.value)} + onChange={(e) => + handleChange('verificationMethod', e.target.value) + } multiline rows={2} placeholder="How to verify this is done?" diff --git a/frontend/src/components/IdeasFilters/IdeasFilters.tsx b/frontend/src/components/IdeasFilters/IdeasFilters.tsx index 26f0ce9..33986b4 100644 --- a/frontend/src/components/IdeasFilters/IdeasFilters.tsx +++ b/frontend/src/components/IdeasFilters/IdeasFilters.tsx @@ -32,7 +32,7 @@ const priorityOptions: { value: IdeaPriority; label: string }[] = [ export function IdeasFilters() { const { filters, setFilter, clearFilters } = useIdeasStore(); const { data: modules = [] } = useModulesQuery(); - const [searchValue, setSearchValue] = useState(filters.search || ''); + const [searchValue, setSearchValue] = useState(filters.search ?? ''); // Debounced search useEffect(() => { @@ -42,31 +42,40 @@ export function IdeasFilters() { return () => clearTimeout(timer); }, [searchValue, setFilter]); - const hasFilters = filters.status || filters.priority || filters.module || filters.search; + const hasFilters = Boolean( + filters.status ?? filters.priority ?? filters.module ?? filters.search, + ); return ( - + setSearchValue(e.target.value)} sx={{ minWidth: 200 }} - InputProps={{ - startAdornment: ( - - - - ), + slotProps={{ + input: { + startAdornment: ( + + + + ), + }, }} /> Status - + value={filters.priority ?? ''} label="Priority" - onChange={(e) => setFilter('priority', e.target.value as IdeaPriority || undefined)} + onChange={(e) => { + const val = e.target.value; + setFilter('priority', val === '' ? undefined : val); + }} > All {priorityOptions.map((opt) => ( @@ -96,7 +108,7 @@ export function IdeasFilters() { Module